Enhance tests on SDK (#17312)

This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
.twenty
|
||||
@@ -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": {
|
||||
|
||||
+15
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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"]
|
||||
}
|
||||
+2
-2
@@ -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);
|
||||
+319
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
+49
@@ -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);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "test-app",
|
||||
"name": "rich-app",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
+1
-1
@@ -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',
|
||||
+9
@@ -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,
|
||||
});
|
||||
+19
@@ -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 (
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
<h1>{message}</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'h0h1h2h3-h4h5-4000-8000-000000000001',
|
||||
name: 'greeting-component',
|
||||
description: 'A component that uses greeting utility',
|
||||
component: GreetingComponent,
|
||||
});
|
||||
+22
@@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
+1
-1
@@ -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 *',
|
||||
},
|
||||
],
|
||||
});
|
||||
+1
-1
@@ -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',
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineFrontComponent } from '@/application/front-components/define-front-component';
|
||||
|
||||
export const RootComponent = () => {
|
||||
return (
|
||||
<div style={{ padding: '10px' }}>
|
||||
<h2>Root Component</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'a0a1a2a3-a4a5-4000-8000-000000000001',
|
||||
name: 'root-component',
|
||||
description: 'A root-level front component',
|
||||
component: RootComponent,
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export const CardDisplay = ({
|
||||
title,
|
||||
content,
|
||||
}: {
|
||||
title: string;
|
||||
content: string;
|
||||
}) => {
|
||||
return (
|
||||
<div style={{ border: '1px solid #ccc', padding: '16px', borderRadius: '8px' }}>
|
||||
<h3>{title}</h3>
|
||||
<p>{content}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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}!`;
|
||||
};
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["../../../../src/*"]
|
||||
"@/*": ["../../../../../src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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<void> {
|
||||
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);
|
||||
|
||||
+2
-6
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+6
-12
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
-208
@@ -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<string, unknown>;
|
||||
const appSources = srcSources['app'] as Record<string, string>;
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@ export class ApplicationEntityBuilder
|
||||
implements ManifestEntityBuilder<Application>
|
||||
{
|
||||
async build(appPath: string): Promise<Application> {
|
||||
const applicationConfigPath = path.join(appPath, 'src', 'app', 'application.config.ts');
|
||||
const applicationConfigPath = path.join(appPath, 'src', 'application.config.ts');
|
||||
|
||||
return manifestExtractFromFileServer.extractManifestFromFile<Application>(applicationConfigPath);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export class FrontComponentEntityBuilder
|
||||
implements ManifestEntityBuilder<FrontComponentManifest[]>
|
||||
{
|
||||
async build(appPath: string): Promise<FrontComponentManifest[]> {
|
||||
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/**'],
|
||||
|
||||
@@ -14,7 +14,7 @@ export class FunctionEntityBuilder
|
||||
implements ManifestEntityBuilder<ServerlessFunctionManifest[]>
|
||||
{
|
||||
async build(appPath: string): Promise<ServerlessFunctionManifest[]> {
|
||||
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/**'],
|
||||
|
||||
@@ -15,7 +15,7 @@ export class ObjectExtensionEntityBuilder
|
||||
implements ManifestEntityBuilder<ObjectExtensionManifest[]>
|
||||
{
|
||||
async build(appPath: string): Promise<ObjectExtensionManifest[]> {
|
||||
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/**'],
|
||||
|
||||
@@ -16,7 +16,7 @@ export class ObjectEntityBuilder
|
||||
implements ManifestEntityBuilder<ObjectManifest[]>
|
||||
{
|
||||
async build(appPath: string): Promise<ObjectManifest[]> {
|
||||
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/**'],
|
||||
|
||||
@@ -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<RoleManifest[]> {
|
||||
async build(appPath: string): Promise<RoleManifest[]> {
|
||||
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/**'],
|
||||
|
||||
@@ -19,25 +19,24 @@ import { validateManifest } from './manifest-validate';
|
||||
import { ManifestValidationError } from './manifest.types';
|
||||
|
||||
const validateFolderStructure = async (appPath: string): Promise<void> => {
|
||||
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<Sources> => {
|
||||
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/**'],
|
||||
|
||||
@@ -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/',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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}'],
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user