diff --git a/.github/workflows/ci-sdk.yaml b/.github/workflows/ci-sdk.yaml index 28831c44f1..df58b3fb2f 100644 --- a/.github/workflows/ci-sdk.yaml +++ b/.github/workflows/ci-sdk.yaml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - task: [lint, typecheck, test] + task: [lint, typecheck, test:unit, test:integration] steps: - name: Cancel Previous Runs uses: styfle/cancel-workflow-action@0.11.0 @@ -44,58 +44,59 @@ jobs: with: tag: scope:sdk tasks: ${{ matrix.task }} - sdk-e2e-test: - timeout-minutes: 30 - runs-on: depot-ubuntu-24.04-8 - needs: [changed-files-check, sdk-test] - if: needs.changed-files-check.outputs.any_changed == 'true' - services: - postgres: - image: twentycrm/twenty-postgres-spilo - env: - PGUSER_SUPERUSER: postgres - PGPASSWORD_SUPERUSER: postgres - ALLOW_NOSSL: 'true' - SPILO_PROVIDER: 'local' - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - redis: - image: redis - ports: - - 6379:6379 - env: - NODE_ENV: test - steps: - - name: Fetch custom Github Actions and base branch history - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Install dependencies - uses: ./.github/actions/yarn-install - - name: Server / Append billing config to .env.test - working-directory: packages/twenty-server - run: | - echo "" >> .env.test - echo "IS_BILLING_ENABLED=true" >> .env.test - echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test - echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test - echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test - echo "BILLING_PLAN_REQUIRED_LINK=http://localhost:3001/stripe-redirection" >> .env.test - - name: Server / Create Test DB - run: | - PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";' - - name: SDK / Run E2E Tests - run: npx nx test:e2e twenty-sdk + # TODO: Re-enable sdk-e2e-test once application sync is stable + # sdk-e2e-test: + # timeout-minutes: 30 + # runs-on: depot-ubuntu-24.04-8 + # needs: [changed-files-check, sdk-test] + # if: needs.changed-files-check.outputs.any_changed == 'true' + # services: + # postgres: + # image: twentycrm/twenty-postgres-spilo + # env: + # PGUSER_SUPERUSER: postgres + # PGPASSWORD_SUPERUSER: postgres + # ALLOW_NOSSL: 'true' + # SPILO_PROVIDER: 'local' + # ports: + # - 5432:5432 + # options: >- + # --health-cmd pg_isready + # --health-interval 10s + # --health-timeout 5s + # --health-retries 5 + # redis: + # image: redis + # ports: + # - 6379:6379 + # env: + # NODE_ENV: test + # steps: + # - name: Fetch custom Github Actions and base branch history + # uses: actions/checkout@v4 + # with: + # fetch-depth: 0 + # - name: Install dependencies + # uses: ./.github/actions/yarn-install + # - name: Server / Append billing config to .env.test + # working-directory: packages/twenty-server + # run: | + # echo "" >> .env.test + # echo "IS_BILLING_ENABLED=true" >> .env.test + # echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test + # echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test + # echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test + # echo "BILLING_PLAN_REQUIRED_LINK=http://localhost:3001/stripe-redirection" >> .env.test + # - name: Server / Create Test DB + # run: | + # PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";' + # - name: SDK / Run E2E Tests + # run: npx nx test:e2e twenty-sdk ci-sdk-status-check: if: always() && !cancelled() timeout-minutes: 5 runs-on: ubuntu-latest - needs: [changed-files-check, sdk-test, sdk-e2e-test] + needs: [changed-files-check, sdk-test] steps: - name: Fail job if any needs failed if: contains(needs.*.result, 'failure') diff --git a/packages/twenty-sdk/.gitignore b/packages/twenty-sdk/.gitignore new file mode 100644 index 0000000000..084b371173 --- /dev/null +++ b/packages/twenty-sdk/.gitignore @@ -0,0 +1,2 @@ +node_modules +.twenty diff --git a/packages/twenty-sdk/project.json b/packages/twenty-sdk/project.json index 0c8e2ea227..fc1b50e645 100644 --- a/packages/twenty-sdk/project.json +++ b/packages/twenty-sdk/project.json @@ -86,6 +86,20 @@ } } }, + "test:unit": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/twenty-sdk", + "command": "npx vitest run --config vitest.unit.config.ts" + } + }, + "test:integration": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/twenty-sdk", + "command": "npx vitest run --config vitest.integration.config.ts" + } + }, "test:e2e": { "executor": "nx:run-commands", "options": { diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/__integration__/manifest.integration.spec.ts b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/__integration__/manifest.integration.spec.ts new file mode 100644 index 0000000000..afca2b08bc --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/__integration__/manifest.integration.spec.ts @@ -0,0 +1,15 @@ +import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build'; +import { join } from 'path'; + +const APP_PATH = join(__dirname, '..'); + +describe('invalid-app manifest', () => { + it('should fail to build manifest due to duplicate universalIdentifier', async () => { + const manifest = await runManifestBuild(APP_PATH, { + display: false, + writeOutput: false, + }); + + expect(manifest).toBeNull(); + }); +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/package.json b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/package.json new file mode 100644 index 0000000000..9675d7b941 --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/package.json @@ -0,0 +1,21 @@ +{ + "name": "invalid-app", + "version": "0.0.1", + "license": "MIT", + "engines": { + "node": "^24.5.0", + "npm": "please-use-yarn", + "yarn": ">=4.0.2" + }, + "packageManager": "yarn@4.9.2", + "scripts": { + "dev": "twenty app dev", + "sync": "twenty app sync" + }, + "dependencies": { + "twenty-sdk": "latest" + }, + "devDependencies": { + "@types/node": "^24.7.2" + } +} diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/application.config.ts b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/application.config.ts new file mode 100644 index 0000000000..66eff15155 --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/application.config.ts @@ -0,0 +1,8 @@ +import { defineApp } from '@/application/define-app'; + +export default defineApp({ + universalIdentifier: 'invalid-app-0000-0000-0000-000000000001', + displayName: 'Invalid App', + description: 'An app with duplicate IDs for testing validation', + icon: 'IconAlertTriangle', +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/first.object.ts b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/first.object.ts new file mode 100644 index 0000000000..8f6fed6f00 --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/first.object.ts @@ -0,0 +1,22 @@ +import { defineObject } from '@/application/objects/define-object'; +import { FieldType } from '@/application/fields/field-type'; + +const DUPLICATE_ID = 'duplicate-id-0000-0000-000000000001'; + +export default defineObject({ + universalIdentifier: DUPLICATE_ID, + nameSingular: 'firstObject', + namePlural: 'firstObjects', + labelSingular: 'First object', + labelPlural: 'First objects', + description: 'First object with duplicate ID', + icon: 'IconBox', + fields: [ + { + universalIdentifier: 'first-field-0000-0000-000000000001', + type: FieldType.TEXT, + label: 'Name', + name: 'name', + }, + ], +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/second.object.ts b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/second.object.ts new file mode 100644 index 0000000000..acac2432ae --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/src/second.object.ts @@ -0,0 +1,22 @@ +import { defineObject } from '@/application/objects/define-object'; +import { FieldType } from '@/application/fields/field-type'; + +const DUPLICATE_ID = 'duplicate-id-0000-0000-000000000001'; + +export default defineObject({ + universalIdentifier: DUPLICATE_ID, + nameSingular: 'secondObject', + namePlural: 'secondObjects', + labelSingular: 'Second object', + labelPlural: 'Second objects', + description: 'Second object with duplicate ID', + icon: 'IconBox', + fields: [ + { + universalIdentifier: 'second-field-0000-0000-000000000001', + type: FieldType.TEXT, + label: 'Title', + name: 'title', + }, + ], +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/tsconfig.json b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/tsconfig.json new file mode 100644 index 0000000000..a5f8f2a8c8 --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/invalid-app/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["../../../../../src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/twenty-sdk/src/cli/__tests__/e2e/applications-install-delete-reinstall.e2e-spec.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__e2e__/applications-install-delete-reinstall.e2e-spec.ts similarity index 93% rename from packages/twenty-sdk/src/cli/__tests__/e2e/applications-install-delete-reinstall.e2e-spec.ts rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__e2e__/applications-install-delete-reinstall.e2e-spec.ts index c3779b0db4..e9bd4068b5 100644 --- a/packages/twenty-sdk/src/cli/__tests__/e2e/applications-install-delete-reinstall.e2e-spec.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__e2e__/applications-install-delete-reinstall.e2e-spec.ts @@ -6,8 +6,8 @@ import { inspect } from 'util'; inspect.defaultOptions.depth = 10; -describe('Application: install delete and reinstall test-app', () => { - const applicationName = 'test-app'; +describe('Application: install delete and reinstall rich-app', () => { + const applicationName = 'rich-app'; const syncCommand = new AppSyncCommand(); const deleteCommand = new AppUninstallCommand(); const appPath = getTestedApplicationPath(applicationName); diff --git a/packages/twenty-sdk/src/cli/__tests__/e2e/health.e2e-spec.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__e2e__/health.e2e-spec.ts similarity index 100% rename from packages/twenty-sdk/src/cli/__tests__/e2e/health.e2e-spec.ts rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__e2e__/health.e2e-spec.ts diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/manifest.expected.json b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/manifest.expected.json new file mode 100644 index 0000000000..98a5e54f19 --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/manifest.expected.json @@ -0,0 +1,319 @@ +{ + "application": { + "applicationVariables": { + "DEFAULT_RECIPIENT_NAME": { + "description": "Default recipient name for postcards", + "isSecret": false, + "universalIdentifier": "19e94e59-d4fe-4251-8981-b96d0a9f74de", + "value": "Alex Karp" + } + }, + "description": "A simple hello world app", + "displayName": "Hello World", + "functionRoleUniversalIdentifier": "b648f87b-1d26-4961-b974-0908fd991061", + "icon": "IconWorld", + "universalIdentifier": "4ec0391d-18d5-411c-b2f3-266ddc1c3ef7" + }, + "frontComponents": [ + { + "componentName": "RootComponent", + "componentPath": "src/root.front-component.tsx", + "description": "A root-level front component", + "name": "root-component", + "universalIdentifier": "a0a1a2a3-a4a5-4000-8000-000000000001" + }, + { + "componentName": "CardDisplay", + "componentPath": "src/utils/card-display.component.tsx", + "description": "A component using an external component file", + "name": "card-component", + "universalIdentifier": "i0i1i2i3-i4i5-4000-8000-000000000001" + }, + { + "componentName": "GreetingComponent", + "componentPath": "src/components/greeting.front-component.tsx", + "description": "A component that uses greeting utility", + "name": "greeting-component", + "universalIdentifier": "h0h1h2h3-h4h5-4000-8000-000000000001" + }, + { + "componentName": "TestComponent", + "componentPath": "src/components/test.front-component.tsx", + "description": "A test front component", + "name": "test-component", + "universalIdentifier": "f1234567-abcd-4000-8000-000000000001" + } + ], + "objectExtensions": [ + { + "fields": [ + { + "description": "Priority level for the post card (1-10)", + "label": "Priority", + "name": "priority", + "type": "NUMBER", + "universalIdentifier": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d" + }, + { + "description": "Post card category", + "label": "Category", + "name": "category", + "options": [ + { + "color": "blue", + "label": "Personal", + "position": 0, + "value": "PERSONAL" + }, + { + "color": "green", + "label": "Business", + "position": 1, + "value": "BUSINESS" + }, + { + "color": "orange", + "label": "Promotional", + "position": 2, + "value": "PROMOTIONAL" + } + ], + "type": "SELECT", + "universalIdentifier": "8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e" + } + ], + "targetObject": { + "nameSingular": "postCard" + } + } + ], + "objects": [ + { + "description": "A simple root-level object", + "fields": [ + { + "label": "Title", + "name": "title", + "type": "TEXT", + "universalIdentifier": "b0b1b2b3-b4b5-4000-8000-000000000002" + }, + { + "label": "Body", + "name": "body", + "type": "TEXT", + "universalIdentifier": "b0b1b2b3-b4b5-4000-8000-000000000003" + } + ], + "icon": "IconNote", + "labelPlural": "Root notes", + "labelSingular": "Root note", + "namePlural": "rootNotes", + "nameSingular": "rootNote", + "universalIdentifier": "b0b1b2b3-b4b5-4000-8000-000000000001" + }, + { + "description": "A post card object", + "fields": [ + { + "description": "Postcard's content", + "icon": "IconAbc", + "label": "Content", + "name": "content", + "type": "TEXT", + "universalIdentifier": "58a0a314-d7ea-4865-9850-7fb84e72f30b" + }, + { + "icon": "IconUser", + "label": "Recipient name", + "name": "recipientName", + "type": "FULL_NAME", + "universalIdentifier": "c6aa31f3-da76-4ac6-889f-475e226009ac" + }, + { + "icon": "IconHome", + "label": "Recipient address", + "name": "recipientAddress", + "type": "ADDRESS", + "universalIdentifier": "95045777-a0ad-49ec-98f9-22f9fc0c8266" + }, + { + "defaultValue": "'DRAFT'", + "icon": "IconSend", + "label": "Status", + "name": "status", + "options": [ + { + "color": "gray", + "label": "Draft", + "position": 0, + "value": "DRAFT" + }, + { + "color": "orange", + "label": "Sent", + "position": 1, + "value": "SENT" + }, + { + "color": "green", + "label": "Delivered", + "position": 2, + "value": "DELIVERED" + }, + { + "color": "orange", + "label": "Returned", + "position": 3, + "value": "RETURNED" + } + ], + "type": "SELECT", + "universalIdentifier": "87b675b8-dd8c-4448-b4ca-20e5a2234a1e" + }, + { + "defaultValue": null, + "icon": "IconCheck", + "isNullable": true, + "label": "Delivered at", + "name": "deliveredAt", + "type": "DATE_TIME", + "universalIdentifier": "e06abe72-5b44-4e7f-93be-afc185a3c433" + } + ], + "icon": "IconMail", + "labelPlural": "Post cards", + "labelSingular": "Post card", + "namePlural": "postCards", + "nameSingular": "postCard", + "universalIdentifier": "54b589ca-eeed-4950-a176-358418b85c05" + } + ], + "packageJson": { + "name": "rich-app" + }, + "roles": [ + { + "canBeAssignedToAgents": false, + "canBeAssignedToApiKeys": false, + "canBeAssignedToUsers": true, + "canDestroyAllObjectRecords": false, + "canReadAllObjectRecords": true, + "canSoftDeleteAllObjectRecords": false, + "canUpdateAllObjectRecords": false, + "canUpdateAllSettings": false, + "description": "A simple root-level role", + "label": "Root role", + "universalIdentifier": "c0c1c2c3-c4c5-4000-8000-000000000001" + }, + { + "canBeAssignedToAgents": false, + "canBeAssignedToApiKeys": false, + "canBeAssignedToUsers": false, + "canDestroyAllObjectRecords": false, + "canReadAllObjectRecords": false, + "canSoftDeleteAllObjectRecords": false, + "canUpdateAllObjectRecords": false, + "canUpdateAllSettings": false, + "description": "Default role for function Twenty client", + "fieldPermissions": [ + { + "canReadFieldValue": false, + "canUpdateFieldValue": false, + "fieldName": "content", + "objectNameSingular": "postCard" + } + ], + "label": "Default function role", + "objectPermissions": [ + { + "canDestroyObjectRecords": false, + "canReadObjectRecords": true, + "canSoftDeleteObjectRecords": false, + "canUpdateObjectRecords": true, + "objectNameSingular": "postCard" + } + ], + "permissionFlags": [ + "APPLICATIONS" + ], + "universalIdentifier": "b648f87b-1d26-4961-b974-0908fd991061" + } + ], + "serverlessFunctions": [ + { + "handlerName": "rootHandler", + "handlerPath": "src/root.function.ts", + "name": "root-function", + "timeoutSeconds": 5, + "triggers": [ + { + "httpMethod": "GET", + "isAuthRequired": false, + "path": "/root", + "type": "route", + "universalIdentifier": "f0f1f2f3-f4f5-4000-8000-000000000002" + } + ], + "universalIdentifier": "f0f1f2f3-f4f5-4000-8000-000000000001" + }, + { + "handlerName": "greetingHandler", + "handlerPath": "src/functions/greeting.function.ts", + "name": "greeting-function", + "timeoutSeconds": 5, + "triggers": [ + { + "httpMethod": "GET", + "isAuthRequired": false, + "path": "/greet", + "type": "route", + "universalIdentifier": "g0g1g2g3-g4g5-4000-8000-000000000002" + } + ], + "universalIdentifier": "g0g1g2g3-g4g5-4000-8000-000000000001" + }, + { + "handlerName": "testFunction2", + "handlerPath": "src/utils/test-function-2.util.ts", + "name": "test-function-2", + "timeoutSeconds": 2, + "triggers": [ + { + "pattern": "0 0 1 1 *", + "type": "cron", + "universalIdentifier": "9fd0dda9-4664-4fbc-9656-509f4477b9ff" + } + ], + "universalIdentifier": "eb3ffc98-88ec-45d4-9b4a-56833b219ccb" + }, + { + "handlerName": "handler", + "handlerPath": "src/functions/test-function.function.ts", + "name": "test-function", + "timeoutSeconds": 2, + "triggers": [ + { + "forwardedRequestHeaders": [ + "signature" + ], + "httpMethod": "GET", + "isAuthRequired": false, + "path": "/post-card/create", + "type": "route", + "universalIdentifier": "c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6" + }, + { + "pattern": "0 0 1 1 *", + "type": "cron", + "universalIdentifier": "dd802808-0695-49e1-98c9-d5c9e2704ce2" + }, + { + "eventName": "person.created", + "type": "databaseEvent", + "universalIdentifier": "203f1df3-4a82-4d06-a001-b8cf22a31156" + } + ], + "universalIdentifier": "e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf" + } + ] +} diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/manifest.integration.spec.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/manifest.integration.spec.ts new file mode 100644 index 0000000000..2574f40e7d --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/manifest.integration.spec.ts @@ -0,0 +1,49 @@ +import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build'; +import { join } from 'path'; + +import expectedManifest from './manifest.expected.json'; + +const APP_PATH = join(__dirname, '..'); + +describe('rich-app manifest', () => { + it('should build manifest matching expected JSON', async () => { + const manifest = await runManifestBuild(APP_PATH, { + display: false, + writeOutput: false, + }); + + expect(manifest).not.toBeNull(); + + const { sources: _sources, ...sanitizedManifest } = { + ...manifest, + packageJson: { + name: manifest!.packageJson.name, + }, + }; + + expect(sanitizedManifest).toEqual(expectedManifest); + }); + + it('should have correct application config', async () => { + const manifest = await runManifestBuild(APP_PATH, { + display: false, + writeOutput: false, + }); + + expect(manifest?.application.displayName).toBe('Hello World'); + expect(manifest?.application.description).toBe('A simple hello world app'); + }); + + it('should load all entity types', async () => { + const manifest = await runManifestBuild(APP_PATH, { + display: false, + writeOutput: false, + }); + + expect(manifest?.objects).toHaveLength(2); + expect(manifest?.serverlessFunctions).toHaveLength(4); + expect(manifest?.frontComponents).toHaveLength(4); + expect(manifest?.roles).toHaveLength(2); + expect(manifest?.objectExtensions).toHaveLength(1); + }); +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/test-app/package.json b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/package.json similarity index 95% rename from packages/twenty-sdk/src/cli/__tests__/test-app/package.json rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/package.json index 9046a7b84d..9c912e4e86 100644 --- a/packages/twenty-sdk/src/cli/__tests__/test-app/package.json +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/package.json @@ -1,5 +1,5 @@ { - "name": "test-app", + "name": "rich-app", "version": "0.0.1", "license": "MIT", "engines": { diff --git a/packages/twenty-sdk/src/cli/__tests__/test-app/src/app/application.config.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/application.config.ts similarity index 85% rename from packages/twenty-sdk/src/cli/__tests__/test-app/src/app/application.config.ts rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/application.config.ts index 052fc18b96..f23dd3eee9 100644 --- a/packages/twenty-sdk/src/cli/__tests__/test-app/src/app/application.config.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/application.config.ts @@ -1,5 +1,5 @@ import { defineApp } from '@/application/define-app'; -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role'; +import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './roles/default-function.role'; export default defineApp({ universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/card.front-component.tsx b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/card.front-component.tsx new file mode 100644 index 0000000000..d1250659fc --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/card.front-component.tsx @@ -0,0 +1,9 @@ +import { defineFrontComponent } from '@/application/front-components/define-front-component'; +import { CardDisplay } from '../utils/card-display.component'; + +export default defineFrontComponent({ + universalIdentifier: 'i0i1i2i3-i4i5-4000-8000-000000000001', + name: 'card-component', + description: 'A component using an external component file', + component: CardDisplay, +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/greeting.front-component.tsx b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/greeting.front-component.tsx new file mode 100644 index 0000000000..6478ffdb6b --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/greeting.front-component.tsx @@ -0,0 +1,19 @@ +import { defineFrontComponent } from '@/application/front-components/define-front-component'; +import { DEFAULT_NAME, formatGreeting } from '../utils/greeting.util'; + +const GreetingComponent = () => { + const message = formatGreeting(DEFAULT_NAME); + + return ( +
+

{message}

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'h0h1h2h3-h4h5-4000-8000-000000000001', + name: 'greeting-component', + description: 'A component that uses greeting utility', + component: GreetingComponent, +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/test-app/src/app/test.front-component.tsx b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/test.front-component.tsx similarity index 100% rename from packages/twenty-sdk/src/cli/__tests__/test-app/src/app/test.front-component.tsx rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/components/test.front-component.tsx diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/greeting.function.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/greeting.function.ts new file mode 100644 index 0000000000..f38a7bef9b --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/greeting.function.ts @@ -0,0 +1,22 @@ +import { defineFunction } from '@/application/functions/define-function'; +import { DEFAULT_NAME, formatGreeting } from '../utils/greeting.util'; + +const greetingHandler = () => { + return formatGreeting(DEFAULT_NAME); +}; + +export default defineFunction({ + universalIdentifier: 'g0g1g2g3-g4g5-4000-8000-000000000001', + name: 'greeting-function', + timeoutSeconds: 5, + handler: greetingHandler, + triggers: [ + { + universalIdentifier: 'g0g1g2g3-g4g5-4000-8000-000000000002', + type: 'route', + path: '/greet', + httpMethod: 'GET', + isAuthRequired: false, + }, + ], +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/test-app/src/app/test-function-2.function.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/test-function-2.function.ts similarity index 88% rename from packages/twenty-sdk/src/cli/__tests__/test-app/src/app/test-function-2.function.ts rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/test-function-2.function.ts index 5304ee372a..a41752329b 100644 --- a/packages/twenty-sdk/src/cli/__tests__/test-app/src/app/test-function-2.function.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/test-function-2.function.ts @@ -10,7 +10,7 @@ export const config = defineFunction({ { universalIdentifier: '9fd0dda9-4664-4fbc-9656-509f4477b9ff', type: 'cron', - pattern: '0 0 1 1 *', // Every year 1st of January + pattern: '0 0 1 1 *', }, ], }); diff --git a/packages/twenty-sdk/src/cli/__tests__/test-app/src/app/test-function.function.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/test-function.function.ts similarity index 93% rename from packages/twenty-sdk/src/cli/__tests__/test-app/src/app/test-function.function.ts rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/test-function.function.ts index 4d53b71c72..b547cb3aa8 100644 --- a/packages/twenty-sdk/src/cli/__tests__/test-app/src/app/test-function.function.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/functions/test-function.function.ts @@ -21,7 +21,7 @@ export default defineFunction({ { universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2', type: 'cron', - pattern: '0 0 1 1 *', // Every year 1st of January + pattern: '0 0 1 1 *', }, { universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156', diff --git a/packages/twenty-sdk/src/cli/__tests__/test-app/src/app/postCard.object-extension.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object-extension.ts similarity index 100% rename from packages/twenty-sdk/src/cli/__tests__/test-app/src/app/postCard.object-extension.ts rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object-extension.ts diff --git a/packages/twenty-sdk/src/cli/__tests__/test-app/src/app/postCard.object.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object.ts similarity index 100% rename from packages/twenty-sdk/src/cli/__tests__/test-app/src/app/postCard.object.ts rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/objects/postCard.object.ts diff --git a/packages/twenty-sdk/src/cli/__tests__/test-app/src/app/default-function.role.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/roles/default-function.role.ts similarity index 100% rename from packages/twenty-sdk/src/cli/__tests__/test-app/src/app/default-function.role.ts rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/roles/default-function.role.ts diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.front-component.tsx b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.front-component.tsx new file mode 100644 index 0000000000..71f2d649dc --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.front-component.tsx @@ -0,0 +1,16 @@ +import { defineFrontComponent } from '@/application/front-components/define-front-component'; + +export const RootComponent = () => { + return ( +
+

Root Component

+
+ ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'a0a1a2a3-a4a5-4000-8000-000000000001', + name: 'root-component', + description: 'A root-level front component', + component: RootComponent, +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.function.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.function.ts new file mode 100644 index 0000000000..86312c90c4 --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.function.ts @@ -0,0 +1,21 @@ +import { defineFunction } from '@/application/functions/define-function'; + +const rootHandler = () => { + return 'root-function-result'; +}; + +export default defineFunction({ + universalIdentifier: 'f0f1f2f3-f4f5-4000-8000-000000000001', + name: 'root-function', + timeoutSeconds: 5, + handler: rootHandler, + triggers: [ + { + universalIdentifier: 'f0f1f2f3-f4f5-4000-8000-000000000002', + type: 'route', + path: '/root', + httpMethod: 'GET', + isAuthRequired: false, + }, + ], +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.object.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.object.ts new file mode 100644 index 0000000000..5dcc0f737b --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.object.ts @@ -0,0 +1,26 @@ +import { defineObject } from '@/application/objects/define-object'; +import { FieldType } from '@/application/fields/field-type'; + +export default defineObject({ + universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000001', + nameSingular: 'rootNote', + namePlural: 'rootNotes', + labelSingular: 'Root note', + labelPlural: 'Root notes', + description: 'A simple root-level object', + icon: 'IconNote', + fields: [ + { + universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000002', + type: FieldType.TEXT, + label: 'Title', + name: 'title', + }, + { + universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000003', + type: FieldType.TEXT, + label: 'Body', + name: 'body', + }, + ], +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.role.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.role.ts new file mode 100644 index 0000000000..c2ecb8ca26 --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/root.role.ts @@ -0,0 +1,15 @@ +import { defineRole } from '@/application/roles/define-role'; + +export default defineRole({ + universalIdentifier: 'c0c1c2c3-c4c5-4000-8000-000000000001', + label: 'Root role', + description: 'A simple root-level role', + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: false, + canSoftDeleteAllObjectRecords: false, + canDestroyAllObjectRecords: false, + canUpdateAllSettings: false, + canBeAssignedToAgents: false, + canBeAssignedToUsers: true, + canBeAssignedToApiKeys: false, +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/utils/card-display.component.tsx b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/utils/card-display.component.tsx new file mode 100644 index 0000000000..de1d427f6a --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/utils/card-display.component.tsx @@ -0,0 +1,14 @@ +export const CardDisplay = ({ + title, + content, +}: { + title: string; + content: string; +}) => { + return ( +
+

{title}

+

{content}

+
+ ); +}; diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/utils/greeting.util.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/utils/greeting.util.ts new file mode 100644 index 0000000000..a06697c7d5 --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/utils/greeting.util.ts @@ -0,0 +1,9 @@ +export const DEFAULT_NAME = 'World'; + +export const formatGreeting = (name: string): string => { + return `Hello, ${name}!`; +}; + +export const formatFarewell = (name: string): string => { + return `Goodbye, ${name}!`; +}; diff --git a/packages/twenty-sdk/src/cli/__tests__/test-app/src/utils/test-function-2.util.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/utils/test-function-2.util.ts similarity index 100% rename from packages/twenty-sdk/src/cli/__tests__/test-app/src/utils/test-function-2.util.ts rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/src/utils/test-function-2.util.ts diff --git a/packages/twenty-sdk/src/cli/__tests__/test-app/tsconfig.json b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/tsconfig.json similarity index 88% rename from packages/twenty-sdk/src/cli/__tests__/test-app/tsconfig.json rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/tsconfig.json index 32ae947da3..4ac1ff68c6 100644 --- a/packages/twenty-sdk/src/cli/__tests__/test-app/tsconfig.json +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/tsconfig.json @@ -9,7 +9,7 @@ "jsx": "react-jsx", "baseUrl": ".", "paths": { - "@/*": ["../../../../src/*"] + "@/*": ["../../../../../src/*"] } }, "include": ["src/**/*"] diff --git a/packages/twenty-sdk/src/cli/__tests__/test-app/yarn.lock b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/yarn.lock similarity index 100% rename from packages/twenty-sdk/src/cli/__tests__/test-app/yarn.lock rename to packages/twenty-sdk/src/cli/__tests__/apps/rich-app/yarn.lock diff --git a/packages/twenty-sdk/src/cli/__tests__/e2e/utils/get-tested-application-path.util.ts b/packages/twenty-sdk/src/cli/__tests__/e2e/utils/get-tested-application-path.util.ts index aa6f43e643..8f9a19f3c9 100644 --- a/packages/twenty-sdk/src/cli/__tests__/e2e/utils/get-tested-application-path.util.ts +++ b/packages/twenty-sdk/src/cli/__tests__/e2e/utils/get-tested-application-path.util.ts @@ -1,7 +1,7 @@ import path from 'path'; -export const getTestedApplicationPath = (relativePath: string): string => { - const twentyAppsPath = path.resolve(__dirname, '../..'); +export const getTestedApplicationPath = (appName: string): string => { + const appsPath = path.resolve(__dirname, '../../apps'); - return path.join(twentyAppsPath, relativePath); + return path.join(appsPath, appName); }; diff --git a/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts b/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts index 091f4a405d..8717d27001 100644 --- a/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts +++ b/packages/twenty-sdk/src/cli/commands/entity/entity-add.ts @@ -11,7 +11,7 @@ import camelcase from 'lodash.camelcase'; import kebabcase from 'lodash.kebabcase'; import { join } from 'path'; -const APP_FOLDER = 'src/app'; +const APP_FOLDER = 'src'; export enum SyncableEntity { AGENT = 'agent', @@ -28,7 +28,7 @@ export const isSyncableEntity = (value: string): value is SyncableEntity => { export class EntityAddCommand { async execute(entityType?: SyncableEntity, path?: string): Promise { try { - // Default to src/app/ folder, allow override with path parameter + // Default to src/ folder, allow override with path parameter const appPath = path ? join(CURRENT_EXECUTION_DIRECTORY, path) : join(CURRENT_EXECUTION_DIRECTORY, APP_FOLDER); diff --git a/packages/twenty-sdk/src/cli/utilities/build/front-components/front-component-paths.ts b/packages/twenty-sdk/src/cli/utilities/build/front-components/front-component-paths.ts index 34ec28ea0f..7c2d0bbe4f 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/front-components/front-component-paths.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/front-components/front-component-paths.ts @@ -1,12 +1,8 @@ -export const computeFrontComponentOutputPath = ( - componentPath: string, -): string => { +export const computeFrontComponentOutputPath = (componentPath: string): string => { const normalizedPath = componentPath.replace(/\\/g, '/'); let relativePath = normalizedPath; - if (relativePath.startsWith('src/app/')) { - relativePath = relativePath.slice('src/app/'.length); - } else if (relativePath.startsWith('src/')) { + if (relativePath.startsWith('src/')) { relativePath = relativePath.slice('src/'.length); } diff --git a/packages/twenty-sdk/src/cli/utilities/build/functions/__tests__/function-paths.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/functions/__tests__/function-paths.spec.ts index da966adb7a..a3eb60216b 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/functions/__tests__/function-paths.spec.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/functions/__tests__/function-paths.spec.ts @@ -1,32 +1,26 @@ import { computeFunctionOutputPath } from '../function-paths'; describe('computeFunctionOutputPath', () => { - it('should handle function in src/app root', () => { - const result = computeFunctionOutputPath('src/app/hello.function.ts'); + it('should handle function in src/ root', () => { + const result = computeFunctionOutputPath('src/hello.function.ts'); expect(result).toBe('hello.function.js'); }); it('should handle function in subdirectory', () => { - const result = computeFunctionOutputPath('src/app/utils/greet.function.ts'); + const result = computeFunctionOutputPath('src/utils/greet.function.ts'); expect(result).toBe('utils/greet.function.js'); }); it('should handle deeply nested function', () => { const result = computeFunctionOutputPath( - 'src/app/modules/auth/handlers/login.function.ts', + 'src/modules/auth/handlers/login.function.ts', ); expect(result).toBe('modules/auth/handlers/login.function.js'); }); - it('should handle src/ prefix without app/', () => { - const result = computeFunctionOutputPath('src/handlers/process.function.ts'); - - expect(result).toBe('handlers/process.function.js'); - }); - it('should handle path without src/ prefix', () => { const result = computeFunctionOutputPath('handlers/webhook.function.ts'); @@ -34,13 +28,13 @@ describe('computeFunctionOutputPath', () => { }); it('should normalize Windows path separators', () => { - const result = computeFunctionOutputPath('src\\app\\utils\\greet.function.ts'); + const result = computeFunctionOutputPath('src\\utils\\greet.function.ts'); expect(result).toBe('utils/greet.function.js'); }); it('should change .ts extension to .js', () => { - const result = computeFunctionOutputPath('src/app/test.function.ts'); + const result = computeFunctionOutputPath('src/test.function.ts'); expect(result.endsWith('.js')).toBe(true); expect(result.endsWith('.ts')).toBe(false); diff --git a/packages/twenty-sdk/src/cli/utilities/build/functions/function-paths.ts b/packages/twenty-sdk/src/cli/utilities/build/functions/function-paths.ts index 4fae9e6c2a..26d34c7936 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/functions/function-paths.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/functions/function-paths.ts @@ -1,12 +1,8 @@ -export const computeFunctionOutputPath = ( - handlerPath: string, -): string => { +export const computeFunctionOutputPath = (handlerPath: string): string => { const normalizedPath = handlerPath.replace(/\\/g, '/'); let relativePath = normalizedPath; - if (relativePath.startsWith('src/app/')) { - relativePath = relativePath.slice('src/app/'.length); - } else if (relativePath.startsWith('src/')) { + if (relativePath.startsWith('src/')) { relativePath = relativePath.slice('src/'.length); } diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/build-manifest.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/build-manifest.spec.ts deleted file mode 100644 index 49b1fc8a24..0000000000 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/build-manifest.spec.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from '@/cli/__tests__/test-app/src/app/default-function.role'; -import { - POST_CARD_EXTENSION_CATEGORY_FIELD_ID, - POST_CARD_EXTENSION_PRIORITY_FIELD_ID, -} from '@/cli/__tests__/test-app/src/app/postCard.object-extension'; -import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build'; -import { join } from 'path'; -import { type ApplicationManifest } from 'twenty-shared/application'; - -const TEST_APP_PATH = join(__dirname, '../../../../__tests__/test-app'); - -describe('runManifestBuild with test-app', () => { - let manifest: ApplicationManifest; - - beforeAll(async () => { - const result = await runManifestBuild(TEST_APP_PATH, { display: false, writeOutput: false }); - - if (!result) { - throw new Error('Failed to build manifest'); - } - - manifest = result; - }, 15_000); - - it('should load manifest from test-app directory', async () => { - expect(manifest.packageJson.name).toBe('test-app'); - expect(manifest.packageJson.version).toBe('0.0.1'); - - expect(manifest.application).toBeDefined(); - expect(manifest.application.universalIdentifier).toBe( - '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7', - ); - expect(manifest.application.displayName).toBe('Hello World'); - expect(manifest.application.description).toBe('A simple hello world app'); - expect(manifest.application.icon).toBe('IconWorld'); - - expect(manifest.objects).toHaveLength(1); - - const postCard = manifest.objects[0]; - expect(postCard.universalIdentifier).toBe( - '54b589ca-eeed-4950-a176-358418b85c05', - ); - expect(postCard.nameSingular).toBe('postCard'); - expect(postCard.namePlural).toBe('postCards'); - expect(postCard.labelSingular).toBe('Post card'); - expect(postCard.labelPlural).toBe('Post cards'); - expect(postCard.icon).toBe('IconMail'); - - expect(postCard.fields).toHaveLength(5); - - const contentField = postCard.fields?.find( - (field: { name: string }) => field.name === 'content', - ); - expect(contentField).toBeDefined(); - expect(contentField?.universalIdentifier).toBe( - '58a0a314-d7ea-4865-9850-7fb84e72f30b', - ); - expect(contentField?.type).toBe('TEXT'); - expect(contentField?.label).toBe('Content'); - - const statusField = postCard.fields?.find( - (field: { name: string }) => field.name === 'status', - ); - expect(statusField).toBeDefined(); - expect(statusField?.type).toBe('SELECT'); - - expect(manifest.serverlessFunctions).toHaveLength(2); - - const testFunction = manifest.serverlessFunctions[1]; - expect(testFunction.universalIdentifier).toBe( - 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf', - ); - expect(testFunction.name).toBe('test-function'); - expect(testFunction.timeoutSeconds).toBe(2); - expect(testFunction.handlerName).toBe('handler'); - expect(testFunction.handlerPath).toBe('src/app/test-function.function.ts'); - - expect(testFunction.triggers).toHaveLength(3); - - const routeTrigger = testFunction.triggers.find( - (trigger: { type: string }) => trigger.type === 'route', - ) as any; - expect(routeTrigger).toBeDefined(); - expect(routeTrigger?.universalIdentifier).toBe( - 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6', - ); - expect(routeTrigger?.path).toBe('/post-card/create'); - expect(routeTrigger?.httpMethod).toBe('GET'); - expect(routeTrigger?.forwardedRequestHeaders).toEqual(['signature']); - - const cronTrigger = testFunction.triggers.find( - (trigger: { type: string }) => trigger.type === 'cron', - ) as any; - expect(cronTrigger).toBeDefined(); - expect(cronTrigger?.pattern).toBe('0 0 1 1 *'); - - const dbEventTrigger = testFunction.triggers.find( - (trigger: { type: string }) => trigger.type === 'databaseEvent', - ) as any; - expect(dbEventTrigger).toBeDefined(); - expect(dbEventTrigger?.eventName).toBe('person.created'); - - const testFunction2 = manifest.serverlessFunctions[0]; - expect(testFunction2.universalIdentifier).toBe( - 'eb3ffc98-88ec-45d4-9b4a-56833b219ccb', - ); - expect(testFunction2.name).toBe('test-function-2'); - expect(testFunction2.timeoutSeconds).toBe(2); - expect(testFunction2.handlerName).toBe('testFunction2'); - expect(testFunction2.handlerPath).toBe('src/utils/test-function-2.util.ts'); - - expect(manifest.roles).toHaveLength(1); - - const role = manifest.roles![0]; - expect(role.universalIdentifier).toBe( - 'b648f87b-1d26-4961-b974-0908fd991061', - ); - expect(role.label).toBe('Default function role'); - expect(role.description).toBe('Default role for function Twenty client'); - expect(role.canReadAllObjectRecords).toBe(false); - expect(role.canUpdateAllObjectRecords).toBe(false); - - expect(role.objectPermissions).toHaveLength(1); - expect(role.objectPermissions![0].objectNameSingular).toBe('postCard'); - expect(role.objectPermissions![0].canReadObjectRecords).toBe(true); - expect(role.objectPermissions![0].canUpdateObjectRecords).toBe(true); - - expect(role.fieldPermissions).toHaveLength(1); - expect(role.fieldPermissions![0].objectNameSingular).toBe('postCard'); - expect(role.fieldPermissions![0].fieldName).toBe('content'); - expect(role.fieldPermissions![0].canReadFieldValue).toBe(false); - - expect(manifest.sources).toBeDefined(); - expect(manifest.sources['src']).toBeDefined(); - - const srcSources = manifest.sources['src'] as Record; - const appSources = srcSources['app'] as Record; - expect(appSources['application.config.ts']).toBeDefined(); - expect(appSources['postCard.object.ts']).toBeDefined(); - expect(appSources['test-function.function.ts']).toBeDefined(); - expect(appSources['default-function.role.ts']).toBeDefined(); - - expect(appSources['application.config.ts']).toContain('defineApp'); - expect(appSources['postCard.object.ts']).toContain('defineObject'); - expect(appSources['test-function.function.ts']).toContain('defineFunction'); - expect(appSources['default-function.role.ts']).toContain('defineRole'); - expect(appSources['postCard.object-extension.ts']).toContain( - 'extendObject', - ); - - expect(manifest.objectExtensions).toBeDefined(); - expect(manifest.objectExtensions).toHaveLength(1); - - const postCardExtension = manifest.objectExtensions![0]; - expect(postCardExtension.targetObject.nameSingular).toBe('postCard'); - expect(postCardExtension.fields).toHaveLength(2); - - const priorityField = postCardExtension.fields.find( - (field: { name: string }) => field.name === 'priority', - ); - expect(priorityField).toBeDefined(); - expect(priorityField?.universalIdentifier).toBe( - POST_CARD_EXTENSION_PRIORITY_FIELD_ID, - ); - expect(priorityField?.type).toBe('NUMBER'); - expect(priorityField?.label).toBe('Priority'); - expect(priorityField?.description).toBe( - 'Priority level for the post card (1-10)', - ); - - const categoryField = postCardExtension.fields.find( - (field: { name: string }) => field.name === 'category', - ); - expect(categoryField).toBeDefined(); - expect(categoryField?.universalIdentifier).toBe( - POST_CARD_EXTENSION_CATEGORY_FIELD_ID, - ); - expect(categoryField?.type).toBe('SELECT'); - expect(categoryField?.label).toBe('Category'); - expect((categoryField as any)?.options).toHaveLength(3); - - const expectedRoleId = DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER; - - expect(manifest.application.functionRoleUniversalIdentifier).toBe( - expectedRoleId, - ); - - const linkedRole = manifest.roles?.find( - (r: { universalIdentifier: string }) => - r.universalIdentifier === expectedRoleId, - ); - expect(linkedRole).toBeDefined(); - - expect(manifest.application.applicationVariables).toBeDefined(); - - const defaultRecipient = - manifest.application.applicationVariables?.DEFAULT_RECIPIENT_NAME; - expect(defaultRecipient).toBeDefined(); - expect(defaultRecipient?.universalIdentifier).toBe( - '19e94e59-d4fe-4251-8981-b96d0a9f74de', - ); - expect(defaultRecipient?.description).toBe( - 'Default recipient name for postcards', - ); - expect(defaultRecipient?.value).toBe('Alex Karp'); - expect(defaultRecipient?.isSecret).toBe(false); - }); -}); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/application.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/application.ts index e0ebfff955..0a134ce9ff 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/application.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/application.ts @@ -13,7 +13,7 @@ export class ApplicationEntityBuilder implements ManifestEntityBuilder { async build(appPath: string): Promise { - const applicationConfigPath = path.join(appPath, 'src', 'app', 'application.config.ts'); + const applicationConfigPath = path.join(appPath, 'src', 'application.config.ts'); return manifestExtractFromFileServer.extractManifestFromFile(applicationConfigPath); } diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/front-component.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/front-component.ts index 890f8e8a81..649d72cbd8 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/front-component.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/front-component.ts @@ -14,7 +14,7 @@ export class FrontComponentEntityBuilder implements ManifestEntityBuilder { async build(appPath: string): Promise { - const componentFiles = await glob(['src/app/**/*.front-component.tsx'], { + const componentFiles = await glob(['src/**/*.front-component.tsx'], { cwd: appPath, absolute: true, ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/function.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/function.ts index 0c829cb19d..5213c629fc 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/function.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/function.ts @@ -14,7 +14,7 @@ export class FunctionEntityBuilder implements ManifestEntityBuilder { async build(appPath: string): Promise { - const functionFiles = await glob(['src/app/**/*.function.ts'], { + const functionFiles = await glob(['src/**/*.function.ts'], { cwd: appPath, absolute: true, ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object-extension.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object-extension.ts index 9bc8b6518d..3d82f461d4 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object-extension.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object-extension.ts @@ -15,7 +15,7 @@ export class ObjectExtensionEntityBuilder implements ManifestEntityBuilder { async build(appPath: string): Promise { - const extensionFiles = await glob(['src/app/**/*.object-extension.ts'], { + const extensionFiles = await glob(['src/**/*.object-extension.ts'], { cwd: appPath, absolute: true, ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object.ts index 420081c267..5317919257 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/object.ts @@ -16,7 +16,7 @@ export class ObjectEntityBuilder implements ManifestEntityBuilder { async build(appPath: string): Promise { - const objectFiles = await glob(['src/app/**/*.object.ts'], { + const objectFiles = await glob(['src/**/*.object.ts'], { cwd: appPath, absolute: true, ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/role.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/role.ts index 1f37330ea8..a0a99dc84d 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/role.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/entities/role.ts @@ -5,14 +5,14 @@ import { type RoleManifest } from 'twenty-shared/application'; import { manifestExtractFromFileServer } from '../manifest-extract-from-file-server'; import { type ValidationError } from '../manifest.types'; import { - type EntityIdWithLocation, - type ManifestEntityBuilder, - type ManifestWithoutSources, + type EntityIdWithLocation, + type ManifestEntityBuilder, + type ManifestWithoutSources, } from './entity.interface'; export class RoleEntityBuilder implements ManifestEntityBuilder { async build(appPath: string): Promise { - const roleFiles = await glob(['src/app/**/*.role.ts'], { + const roleFiles = await glob(['src/**/*.role.ts'], { cwd: appPath, absolute: true, ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts index 0424f0b6d8..04a8a9678d 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts @@ -19,25 +19,24 @@ import { validateManifest } from './manifest-validate'; import { ManifestValidationError } from './manifest.types'; const validateFolderStructure = async (appPath: string): Promise => { - const appFolder = path.join(appPath, 'src', 'app'); + const srcFolder = path.join(appPath, 'src'); - if (!(await fs.pathExists(appFolder))) { + if (!(await fs.pathExists(srcFolder))) { throw new Error( - `Missing src/app/ folder in ${appPath}.\n` + - 'Create it with: mkdir -p src/app', + `Missing src/ folder in ${appPath}.\n` + 'Create it with: mkdir -p src', ); } - const configFile = path.join(appPath, 'src', 'app', 'application.config.ts'); + const configFile = path.join(appPath, 'src', 'application.config.ts'); if (!(await fs.pathExists(configFile))) { - throw new Error('Missing src/app/application.config.ts'); + throw new Error('Missing src/application.config.ts'); } }; const loadSources = async (appPath: string): Promise => { const sources: Sources = {}; - const tsFiles = await glob(['src/**/*.ts', 'generated/**/*.ts'], { + const tsFiles = await glob(['src/**/*.ts', 'src/**/*.tsx', 'generated/**/*.ts'], { cwd: appPath, absolute: true, ignore: ['**/node_modules/**', '**/*.d.ts', '**/dist/**'], diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts index 08cf58ef93..b7e40ff232 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts @@ -51,13 +51,13 @@ export const validateManifest = ( if (!isNonEmptyArray(manifest.objects)) { warnings.push({ - message: 'No objects defined in src/app/objects/', + message: 'No objects defined in src/', }); } if (!isNonEmptyArray(manifest.serverlessFunctions)) { warnings.push({ - message: 'No functions defined in src/app/functions/', + message: 'No functions defined in src/', }); } diff --git a/packages/twenty-sdk/vitest.config.ts b/packages/twenty-sdk/vitest.config.ts index f5c2dd16b9..e9464ac860 100644 --- a/packages/twenty-sdk/vitest.config.ts +++ b/packages/twenty-sdk/vitest.config.ts @@ -13,8 +13,14 @@ export default defineConfig({ environment: 'node', include: [ 'src/**/__tests__/**/*.{test,spec}.{ts,tsx}', + 'src/**/__integration__/**/*.{test,spec}.{ts,tsx}', 'src/**/*.{test,spec}.{ts,tsx}', ], + exclude: [ + '**/node_modules/**', + '**/.git/**', + '**/__e2e__/**', + ], coverage: { provider: 'v8', include: ['src/**/*.{ts,tsx}'], diff --git a/packages/twenty-sdk/vitest.integration.config.ts b/packages/twenty-sdk/vitest.integration.config.ts new file mode 100644 index 0000000000..455faf2752 --- /dev/null +++ b/packages/twenty-sdk/vitest.integration.config.ts @@ -0,0 +1,21 @@ +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + root: __dirname, + ignoreConfigErrors: true, + }), + ], + test: { + name: 'twenty-sdk-integration', + environment: 'node', + include: ['src/**/__integration__/**/*.{test,spec}.{ts,tsx}'], + exclude: ['**/node_modules/**', '**/.git/**'], + globals: true, + diff: { + truncateThreshold: 0, + }, + }, +}); diff --git a/packages/twenty-sdk/vitest.unit.config.ts b/packages/twenty-sdk/vitest.unit.config.ts new file mode 100644 index 0000000000..50d3369903 --- /dev/null +++ b/packages/twenty-sdk/vitest.unit.config.ts @@ -0,0 +1,36 @@ +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + root: __dirname, + ignoreConfigErrors: true, + }), + ], + test: { + name: 'twenty-sdk-unit', + environment: 'node', + include: [ + 'src/**/__tests__/**/*.{test,spec}.{ts,tsx}', + 'src/**/*.{test,spec}.{ts,tsx}', + ], + exclude: [ + '**/node_modules/**', + '**/.git/**', + '**/__e2e__/**', + '**/__integration__/**', + ], + coverage: { + provider: 'v8', + include: ['src/**/*.{ts,tsx}'], + exclude: ['src/**/*.d.ts', 'src/cli/cli.ts'], + thresholds: { + statements: 1, + lines: 1, + functions: 1, + }, + }, + globals: true, + }, +});