Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81d192333a |
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"postgres": {
|
||||
"type": "stdio",
|
||||
"command": "bash",
|
||||
"args": ["-c", "source packages/twenty-server/.env && npx -y @modelcontextprotocol/server-postgres \"$PG_DATABASE_URL\""],
|
||||
"env": {}
|
||||
},
|
||||
"playwright": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--no-sandbox", "--headless"],
|
||||
"env": {}
|
||||
},
|
||||
"context7": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@upstash/context7-mcp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Twenty is an open-source CRM built with modern technologies in a monorepo structure. The codebase is organized as an Nx workspace with multiple packages.
|
||||
|
||||
## Key Commands
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# Start development environment (frontend + backend + worker)
|
||||
yarn start
|
||||
|
||||
# Individual package development
|
||||
npx nx start twenty-front # Start frontend dev server
|
||||
npx nx start twenty-server # Start backend server
|
||||
npx nx run twenty-server:worker # Start background worker
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Preferred: run a single test file (fast)
|
||||
npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
|
||||
|
||||
# Run all tests for a package
|
||||
npx nx test twenty-front # Frontend unit tests
|
||||
npx nx test twenty-server # Backend unit tests
|
||||
npx nx run twenty-server:test:integration:with-db-reset # Integration tests with DB reset
|
||||
# To run an indivual test or a pattern of tests, use the following command:
|
||||
cd packages/{workspace} && npx jest "pattern or filename"
|
||||
|
||||
# Storybook
|
||||
npx nx storybook:build twenty-front
|
||||
npx nx storybook:test twenty-front
|
||||
|
||||
# When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Linting (diff with main - fastest, always prefer this)
|
||||
npx nx lint:diff-with-main twenty-front
|
||||
npx nx lint:diff-with-main twenty-server
|
||||
npx nx lint:diff-with-main twenty-front --configuration=fix # Auto-fix
|
||||
|
||||
# Linting (full project - slower, use only when needed)
|
||||
npx nx lint twenty-front
|
||||
npx nx lint twenty-server
|
||||
|
||||
# Type checking
|
||||
npx nx typecheck twenty-front
|
||||
npx nx typecheck twenty-server
|
||||
|
||||
# Format code
|
||||
npx nx fmt twenty-front
|
||||
npx nx fmt twenty-server
|
||||
```
|
||||
|
||||
### Build
|
||||
```bash
|
||||
# Build packages (twenty-shared must be built first)
|
||||
npx nx build twenty-shared
|
||||
npx nx build twenty-front
|
||||
npx nx build twenty-server
|
||||
```
|
||||
|
||||
### Database Operations
|
||||
```bash
|
||||
# Database management
|
||||
npx nx database:reset twenty-server # Reset database
|
||||
npx nx run twenty-server:database:init:prod # Initialize database
|
||||
npx nx run twenty-server:database:migrate:prod # Run instance commands (fast only)
|
||||
|
||||
# Generate an instance command (fast or slow)
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
|
||||
```
|
||||
|
||||
### Database Inspection (Postgres MCP)
|
||||
|
||||
A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
|
||||
- Inspect workspace data, metadata, and object definitions while developing
|
||||
- Verify migration results (columns, types, constraints) after running migrations
|
||||
- Explore the multi-tenant schema structure (core, metadata, workspace-specific schemas)
|
||||
- Debug issues by querying raw data to confirm whether a bug is frontend, backend, or data-level
|
||||
- Inspect metadata tables to debug GraphQL schema generation issues
|
||||
|
||||
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
|
||||
|
||||
### GraphQL
|
||||
```bash
|
||||
# Generate GraphQL types (run after schema changes)
|
||||
npx nx run twenty-front:graphql:generate
|
||||
npx nx run twenty-front:graphql:generate --configuration=metadata
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Tech Stack
|
||||
- **Frontend**: React 18, TypeScript, Jotai (state management), Linaria (styling), Vite
|
||||
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL (with GraphQL Yoga)
|
||||
- **Monorepo**: Nx workspace managed with Yarn 4
|
||||
|
||||
### Package Structure
|
||||
```
|
||||
packages/
|
||||
├── twenty-front/ # React frontend application
|
||||
├── twenty-server/ # NestJS backend API
|
||||
├── twenty-ui/ # Shared UI components library
|
||||
├── twenty-shared/ # Common types and utilities
|
||||
├── twenty-emails/ # Email templates with React Email
|
||||
├── twenty-website/ # Next.js documentation website
|
||||
├── twenty-zapier/ # Zapier integration
|
||||
└── twenty-e2e-testing/ # Playwright E2E tests
|
||||
```
|
||||
|
||||
### Key Development Principles
|
||||
- **Functional components only** (no class components)
|
||||
- **Named exports only** (no default exports)
|
||||
- **Types over interfaces** (except when extending third-party interfaces)
|
||||
- **String literals over enums** (except for GraphQL enums)
|
||||
- **No 'any' type allowed** — strict TypeScript enforced
|
||||
- **Event handlers preferred over useEffect** for state updates
|
||||
- **Props down, events up** — unidirectional data flow
|
||||
- **Composition over inheritance**
|
||||
- **No abbreviations** in variable names (`user` not `u`, `fieldMetadata` not `fm`)
|
||||
|
||||
### Naming Conventions
|
||||
- **Variables/functions**: camelCase
|
||||
- **Constants**: SCREAMING_SNAKE_CASE
|
||||
- **Types/Classes**: PascalCase (suffix component props with `Props`, e.g. `ButtonProps`)
|
||||
- **Files/directories**: kebab-case with descriptive suffixes (`.component.tsx`, `.service.ts`, `.entity.ts`, `.dto.ts`, `.module.ts`)
|
||||
- **TypeScript generics**: descriptive names (`TData` not `T`)
|
||||
|
||||
### File Structure
|
||||
- Components under 300 lines, services under 500 lines
|
||||
- Components in their own directories with tests and stories
|
||||
- Use `index.ts` barrel exports for clean imports
|
||||
- Import order: external libraries first, then internal (`@/`), then relative
|
||||
|
||||
### Comments
|
||||
- Use short-form comments (`//`), not JSDoc blocks
|
||||
- Explain WHY (business logic), not WHAT
|
||||
- Do not comment obvious code
|
||||
- Multi-line comments use multiple `//` lines, not `/** */`
|
||||
|
||||
### State Management
|
||||
- **Jotai** for global state: atoms for primitive state, selectors for derived state, atom families for dynamic collections
|
||||
- Component-specific state with React hooks (`useState`, `useReducer` for complex logic)
|
||||
- GraphQL cache managed by Apollo Client
|
||||
- Use functional state updates: `setState(prev => prev + 1)`
|
||||
|
||||
### Backend Architecture
|
||||
- **NestJS modules** for feature organization
|
||||
- **TypeORM** for database ORM with PostgreSQL
|
||||
- **GraphQL** API with code-first approach
|
||||
- **Redis** for caching and session management
|
||||
- **BullMQ** for background job processing
|
||||
|
||||
### Database & Upgrade Commands
|
||||
- **PostgreSQL** as primary database
|
||||
- **Redis** for caching and sessions
|
||||
- **ClickHouse** for analytics (when enabled)
|
||||
- When changing entity files, generate an **instance command** (`database:migrate:generate --name <name> --type <fast|slow>`)
|
||||
- **Fast** instance commands handle schema changes; **slow** ones add a `runDataMigration` step for data backfills
|
||||
- **Workspace commands** iterate over all active/suspended workspaces for per-workspace upgrades
|
||||
- Commands use `@RegisteredInstanceCommand` and `@RegisteredWorkspaceCommand` decorators for automatic discovery
|
||||
- Include both `up` and `down` logic in instance commands
|
||||
- Never delete or rewrite committed instance command `up`/`down` logic
|
||||
- See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation
|
||||
|
||||
### Utility Helpers
|
||||
Use existing helpers from `twenty-shared` instead of manual type guards:
|
||||
- `isDefined()`, `isNonEmptyString()`, `isNonEmptyArray()`
|
||||
|
||||
## Development Workflow
|
||||
|
||||
IMPORTANT: Use Context7 for code generation, setup or configuration steps, or library/API documentation. Automatically use the Context7 MCP tools to resolve library IDs and get library docs without waiting for explicit requests.
|
||||
|
||||
### Before Making Changes
|
||||
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
|
||||
2. Test changes with relevant test suites (prefer single-file test runs)
|
||||
3. Ensure instance commands are generated for entity changes (`database:migrate:generate`)
|
||||
4. Check that GraphQL schema changes are backward compatible
|
||||
5. Run `graphql:generate` after any GraphQL schema changes
|
||||
|
||||
### Code Style Notes
|
||||
- Use **Linaria** for styling with zero-runtime CSS-in-JS (styled-components pattern)
|
||||
- Follow **Nx** workspace conventions for imports
|
||||
- Use **Lingui** for internationalization
|
||||
- Apply security first, then formatting (sanitize before format)
|
||||
|
||||
### Testing Strategy
|
||||
- **Test behavior, not implementation** — focus on user perspective
|
||||
- **Test pyramid**: 70% unit, 20% integration, 10% E2E
|
||||
- Query by user-visible elements (text, roles, labels) over test IDs
|
||||
- Use `@testing-library/user-event` for realistic interactions
|
||||
- Descriptive test names: "should [behavior] when [condition]"
|
||||
- Clear mocks between tests with `jest.clearAllMocks()`
|
||||
|
||||
## Dev Environment Setup
|
||||
|
||||
All dev environments (Claude Code web, Cursor, local) use one script:
|
||||
|
||||
```bash
|
||||
bash packages/twenty-utils/setup-dev-env.sh
|
||||
```
|
||||
|
||||
This handles everything: starts Postgres + Redis (auto-detects local services vs Docker), creates databases, and copies `.env` files. Idempotent — safe to run multiple times.
|
||||
|
||||
- `--docker` — force Docker mode (uses `packages/twenty-docker/docker-compose.dev.yml`)
|
||||
- `--down` — stop services
|
||||
- `--reset` — wipe data and restart fresh
|
||||
- **Skip the setup script** for tasks that only read code — architecture questions, code review, documentation, etc.
|
||||
|
||||
**Note:** CI workflows (GitHub Actions) manage services via Actions service containers and run setup steps individually — they don't use this script.
|
||||
|
||||
## Important Files
|
||||
- `nx.json` - Nx workspace configuration with task definitions
|
||||
- `tsconfig.base.json` - Base TypeScript configuration
|
||||
- `package.json` - Root package with workspace definitions
|
||||
- `.cursor/rules/` - Detailed development guidelines and best practices
|
||||
@@ -12,7 +12,7 @@ This directory contains Twenty's development guidelines and best practices in th
|
||||
### Core Guidelines
|
||||
- **architecture.mdc** - Project overview, technology stack, and infrastructure setup (Always Applied)
|
||||
- **nx-rules.mdc** - Nx workspace guidelines and best practices (Auto-attached to Nx files)
|
||||
- **server-migrations.mdc** - Upgrade command guidelines (instance commands and workspace commands) for `twenty-server` (Auto-attached to server entities and upgrade command files)
|
||||
- **server-migrations.mdc** - Backend migration and TypeORM guidelines for `twenty-server` (Auto-attached to server entities and migration files)
|
||||
- **creating-syncable-entity.mdc** - Comprehensive guide for creating new syncable entities (with universalIdentifier and applicationId) in the workspace migration system (Agent-requested for metadata-modules and workspace-migration files)
|
||||
|
||||
### Code Quality
|
||||
@@ -81,8 +81,10 @@ npx nx run twenty-server:typecheck # Type checking
|
||||
npx nx run twenty-server:test # Run unit tests
|
||||
npx nx run twenty-server:test:integration:with-db-reset # Run integration tests
|
||||
|
||||
# Upgrade commands (instance + workspace)
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
|
||||
# Migrations
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/[name] -d src/database/typeorm/core/core.datasource.ts
|
||||
|
||||
# Workspace
|
||||
```
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
@@ -55,8 +55,7 @@ If feature descriptions are not provided or need enhancement, research the codeb
|
||||
- Services: Look for `*.service.ts` files
|
||||
|
||||
**For Database/ORM Changes:**
|
||||
- Instance commands (fast/slow): `packages/twenty-server/src/database/commands/upgrade-version-command/`
|
||||
- Legacy TypeORM migrations: `packages/twenty-server/src/database/typeorm/`
|
||||
- Migrations: `packages/twenty-server/src/database/typeorm/`
|
||||
- Entities: `packages/twenty-server/src/entities/`
|
||||
|
||||
### Research Commands
|
||||
|
||||
@@ -22,7 +22,7 @@ This main guide provides a high-level overview and navigation hub.
|
||||
|
||||
A syncable entity is a metadata entity that:
|
||||
- Has a **`universalIdentifier`**: A unique identifier used for syncing entities across workspaces/applications
|
||||
- Has an **`applicationId`**: Links the entity to an application (Standard or Custom applications)
|
||||
- Has an **`applicationId`**: Links the entity to an application (Twenty Standard or Custom applications)
|
||||
- Participates in the **workspace migration system**: Can be created, updated, and deleted through the migration pipeline
|
||||
- Is **cached as a flat entity**: Denormalized representation for efficient validation and change detection
|
||||
|
||||
|
||||
@@ -1,46 +1,29 @@
|
||||
---
|
||||
description: Guidelines for generating and managing upgrade commands (instance commands and workspace commands) in twenty-server
|
||||
description: Guidelines for generating and managing TypeORM migrations in twenty-server
|
||||
globs: [
|
||||
"packages/twenty-server/src/**/*.entity.ts",
|
||||
"packages/twenty-server/src/database/commands/upgrade-version-command/**/*.ts"
|
||||
"packages/twenty-server/src/database/typeorm/**/*.ts"
|
||||
]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
## Upgrade Commands (twenty-server)
|
||||
## Server Migrations (twenty-server)
|
||||
|
||||
The upgrade system uses two types of commands instead of raw TypeORM migrations:
|
||||
- **Instance commands** — schema and data migrations that run once at the instance level.
|
||||
- **Workspace commands** — commands that iterate over all active/suspended workspaces.
|
||||
|
||||
See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation.
|
||||
|
||||
### Instance Commands
|
||||
|
||||
- **When changing a `*.entity.ts` file**, generate an instance command:
|
||||
- **When changing an entity, always generate a migration**
|
||||
- If you modify a `*.entity.ts` file in `packages/twenty-server/src`, you **must** generate a corresponding TypeORM migration instead of manually editing the database schema.
|
||||
- Use the Nx + TypeORM command from the project root:
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
|
||||
```
|
||||
|
||||
- **Fast commands** (`--type fast`, default) are for schema-only changes that must run immediately. They implement `FastInstanceCommand` with `up`/`down` methods and use the `@RegisteredInstanceCommand` decorator.
|
||||
- Replace `[name]` with a descriptive, kebab-case migration name that reflects the change (for example, `add-agent-turn-evaluation`).
|
||||
|
||||
- **Slow commands** (`--type slow`) add a `runDataMigration` method for potentially long-running data backfills that execute before `up`. They only run when `--include-slow` is passed. Use the decorator with `{ type: 'slow' }`.
|
||||
- **Prefer generated migrations over manual edits**
|
||||
- Let TypeORM infer schema changes from the updated entities; only adjust the generated migration file manually if absolutely necessary (for example, for data backfills or complex constraints).
|
||||
- Keep schema changes (DDL) in these generated migrations and avoid mixing in heavy data migrations unless there is a strong reason and clear comments.
|
||||
|
||||
- The generator auto-registers the command in `instance-commands.constant.ts` — do not edit that file manually.
|
||||
|
||||
- **Keep commands consistent and reversible**: include both `up` and `down` logic. Do not delete or rewrite existing, committed commands unless on a pre-release branch.
|
||||
|
||||
### Workspace Commands
|
||||
|
||||
- Use the `@RegisteredWorkspaceCommand` decorator alongside nest-commander's `@Command` decorator.
|
||||
- Extend `ActiveOrSuspendedWorkspaceCommandRunner` and implement `runOnWorkspace`.
|
||||
- The base class provides `--dry-run`, `--verbose`, and workspace filter options automatically.
|
||||
|
||||
### Execution Order
|
||||
|
||||
Within a given version, commands run in this order (timestamp-sorted within each group):
|
||||
1. Instance fast commands
|
||||
2. Instance slow commands (only with `--include-slow`)
|
||||
3. Workspace commands
|
||||
- **Keep migrations consistent and reversible**
|
||||
- Ensure the generated migration includes both `up` and `down` logic that correctly applies and reverts the entity change when possible.
|
||||
- Do not delete or rewrite existing, committed migrations unless you are explicitly working on a pre-release branch where history rewrites are allowed by team conventions.
|
||||
|
||||
|
||||
@@ -8,10 +8,6 @@ inputs:
|
||||
api-key:
|
||||
description: API key or access token for the target instance
|
||||
required: true
|
||||
app-path:
|
||||
description: Path to the app directory (relative to repo root). Defaults to repo root.
|
||||
required: false
|
||||
default: '.'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -23,13 +19,11 @@ runs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '${{ inputs.app-path }}/.nvmrc'
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
cache-dependency-path: '${{ inputs.app-path }}/yarn.lock'
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.app-path }}
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Configure remote
|
||||
@@ -49,5 +43,4 @@ runs:
|
||||
|
||||
- name: Deploy
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.app-path }}
|
||||
run: yarn twenty deploy --remote target
|
||||
|
||||
@@ -8,10 +8,6 @@ inputs:
|
||||
api-key:
|
||||
description: API key or access token for the target workspace
|
||||
required: true
|
||||
app-path:
|
||||
description: Path to the app directory (relative to repo root). Defaults to repo root.
|
||||
required: false
|
||||
default: '.'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -23,13 +19,11 @@ runs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '${{ inputs.app-path }}/.nvmrc'
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
cache-dependency-path: '${{ inputs.app-path }}/yarn.lock'
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.app-path }}
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Configure remote
|
||||
@@ -49,5 +43,4 @@ runs:
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.app-path }}
|
||||
run: yarn twenty install --remote target
|
||||
|
||||
@@ -15,8 +15,8 @@ outputs:
|
||||
description: 'URL where the Twenty test server can be reached'
|
||||
value: http://localhost:2021
|
||||
api-key:
|
||||
description: 'API key (type: API_KEY) for the seeded Twenty dev workspace'
|
||||
value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc
|
||||
description: 'API key for the Twenty test instance'
|
||||
value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjQ5MDQ4ODE3MDR9.9S4wc0MOr5iczsomlFxZdOHD1IRDS4dnRSwNVNpctF4
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
|
||||
@@ -251,14 +251,6 @@ jobs:
|
||||
rm -f /tmp/current-server.pid
|
||||
fi
|
||||
|
||||
- name: Flush Redis between server runs
|
||||
run: |
|
||||
# Clear all Redis caches to prevent stale data from the current branch
|
||||
# server contaminating the main branch server. Both servers share the
|
||||
# same Redis instance, and CoreEntityCacheService/WorkspaceCacheService
|
||||
# persist cached entities across process restarts.
|
||||
redis-cli -h localhost -p 6379 FLUSHALL || echo "::warning::Failed to flush Redis"
|
||||
|
||||
- name: Checkout main branch
|
||||
run: |
|
||||
git stash
|
||||
|
||||
@@ -139,19 +139,11 @@ jobs:
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty --version
|
||||
|
||||
- name: Setup server environment
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
- name: Create test database
|
||||
run: PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
|
||||
- name: Create databases
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
|
||||
- name: Setup database
|
||||
run: npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: nohup npx nx start:ci twenty-server &
|
||||
- name: Setup database and start server
|
||||
run: npx nx run twenty-server:database:reset && nohup npx nx start:ci twenty-server &
|
||||
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
|
||||
|
||||
@@ -133,19 +133,11 @@ jobs:
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty --version
|
||||
|
||||
- name: Setup server environment
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
- name: Create test database
|
||||
run: PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
|
||||
- name: Create databases
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
|
||||
- name: Setup database
|
||||
run: npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: nohup npx nx start:ci twenty-server &
|
||||
- name: Setup database and start server
|
||||
run: npx nx run twenty-server:database:reset && nohup npx nx start:ci twenty-server &
|
||||
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
|
||||
@@ -155,7 +147,17 @@ jobs:
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
|
||||
|
||||
- name: Run scaffolded app integration test (deploys, installs, and verifies the app)
|
||||
- name: Deploy scaffolded app
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty deploy
|
||||
|
||||
- name: Install scaffolded app
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty install
|
||||
|
||||
- name: Run scaffolded app integration test
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
yarn test
|
||||
|
||||
@@ -137,19 +137,11 @@ jobs:
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty --version
|
||||
|
||||
- name: Setup server environment
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
- name: Create test database
|
||||
run: PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
|
||||
- name: Create databases
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
|
||||
- name: Setup database
|
||||
run: npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: nohup npx nx start:ci twenty-server &
|
||||
- name: Setup database and start server
|
||||
run: npx nx run twenty-server:database:reset && nohup npx nx start:ci twenty-server &
|
||||
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
|
||||
|
||||
@@ -62,19 +62,11 @@ jobs:
|
||||
- name: Build SDK packages
|
||||
run: npx nx build twenty-sdk
|
||||
|
||||
- name: Setup server environment
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
- name: Create test database
|
||||
run: PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
|
||||
- name: Create databases
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
|
||||
- name: Setup database
|
||||
run: npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: nohup npx nx start:ci twenty-server &
|
||||
- name: Setup database and start server
|
||||
run: npx nx run twenty-server:database:reset && nohup npx nx start:ci twenty-server &
|
||||
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
|
||||
|
||||
@@ -62,19 +62,11 @@ jobs:
|
||||
- name: Build SDK packages
|
||||
run: npx nx build twenty-sdk
|
||||
|
||||
- name: Setup server environment
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
- name: Create test database
|
||||
run: PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
|
||||
- name: Create databases
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
|
||||
- name: Setup database
|
||||
run: npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: nohup npx nx start:ci twenty-server &
|
||||
- name: Setup database and start server
|
||||
run: npx nx run twenty-server:database:reset && nohup npx nx start:ci twenty-server &
|
||||
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
|
||||
|
||||
@@ -19,7 +19,6 @@ jobs:
|
||||
files: |
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-server/**
|
||||
.github/workflows/ci-sdk.yaml
|
||||
!packages/twenty-sdk/package.json
|
||||
sdk-test:
|
||||
needs: changed-files-check
|
||||
@@ -70,6 +69,7 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
env:
|
||||
NODE_ENV: test
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
|
||||
steps:
|
||||
@@ -79,26 +79,16 @@ jobs:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build SDK
|
||||
- name: Build
|
||||
run: npx nx build twenty-sdk
|
||||
- name: Setup server environment
|
||||
run: npx nx reset:env:e2e-testing-server twenty-server
|
||||
- name: Create databases
|
||||
- name: Server / Create Test DB
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
- name: Setup database
|
||||
run: npx nx run twenty-server:database:reset
|
||||
- name: Start server
|
||||
run: nohup npx nx start:ci twenty-server > /tmp/twenty-server.log 2>&1 &
|
||||
- name: Wait for server to be ready
|
||||
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
|
||||
- name: SDK / Run e2e Tests
|
||||
run: NODE_ENV=test npx vitest run --config ./vitest.e2e.config.ts
|
||||
working-directory: packages/twenty-sdk
|
||||
- name: Server / Dump logs on failure
|
||||
if: failure()
|
||||
run: tail -100 /tmp/twenty-server.log || echo "No server log file found"
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
tag: scope:sdk
|
||||
tasks: test:e2e
|
||||
ci-sdk-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
|
||||
@@ -144,18 +144,15 @@ jobs:
|
||||
exit 1
|
||||
- name: Server / Check for Pending Migrations
|
||||
run: |
|
||||
npx nx database:migrate:generate twenty-server -- --name pending-migration-check || true
|
||||
CORE_MIGRATION_OUTPUT=$(npx nx database:migrate:generate twenty-server -- --name core-migration-check || true)
|
||||
|
||||
if ! git diff --quiet; then
|
||||
CORE_MIGRATION_FILE=$(ls packages/twenty-server/src/database/typeorm/core/migrations/common/*core-migration-check.ts 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$CORE_MIGRATION_FILE" ]; then
|
||||
echo "::error::Unexpected migration files were generated. Please run 'npx nx database:migrate:generate twenty-server -- --name <migration-name>' and commit the result."
|
||||
echo ""
|
||||
echo "The following migration changes were detected:"
|
||||
echo "==================================================="
|
||||
git diff
|
||||
echo "==================================================="
|
||||
echo ""
|
||||
echo "$CORE_MIGRATION_OUTPUT"
|
||||
|
||||
git checkout -- .
|
||||
rm -f packages/twenty-server/src/database/typeorm/core/migrations/common/*core-migration-check.ts
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -69,14 +69,13 @@ jobs:
|
||||
|
||||
- name: Server / Start
|
||||
run: |
|
||||
npx nx run twenty-server:start:ci &
|
||||
npx nx start twenty-server &
|
||||
echo "Waiting for server to be ready..."
|
||||
timeout 60 bash -c 'until curl -sf http://localhost:3000/healthz; do sleep 2; done'
|
||||
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
|
||||
|
||||
- name: Start worker
|
||||
working-directory: packages/twenty-server
|
||||
run: |
|
||||
NODE_ENV=development node dist/queue-worker/queue-worker.js &
|
||||
npx nx run twenty-server:worker &
|
||||
echo "Worker started"
|
||||
|
||||
- name: Zapier / Build
|
||||
|
||||
@@ -53,4 +53,3 @@ mcp.json
|
||||
TRANSLATION_QA_REPORT.md
|
||||
.playwright-mcp/
|
||||
.playwright-cli/
|
||||
output/playwright/
|
||||
|
||||
@@ -71,10 +71,10 @@ npx nx build twenty-server
|
||||
# Database management
|
||||
npx nx database:reset twenty-server # Reset database
|
||||
npx nx run twenty-server:database:init:prod # Initialize database
|
||||
npx nx run twenty-server:database:migrate:prod # Run instance commands (fast only)
|
||||
npx nx run twenty-server:database:migrate:prod # Run migrations
|
||||
|
||||
# Generate an instance command (fast or slow)
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
|
||||
# Generate migration (replace [name] with kebab-case descriptive name)
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
|
||||
```
|
||||
|
||||
### Database Inspection (Postgres MCP)
|
||||
@@ -158,17 +158,14 @@ packages/
|
||||
- **Redis** for caching and session management
|
||||
- **BullMQ** for background job processing
|
||||
|
||||
### Database & Upgrade Commands
|
||||
### Database & Migrations
|
||||
- **PostgreSQL** as primary database
|
||||
- **Redis** for caching and sessions
|
||||
- **ClickHouse** for analytics (when enabled)
|
||||
- When changing entity files, generate an **instance command** (`database:migrate:generate --name <name> --type <fast|slow>`)
|
||||
- **Fast** instance commands handle schema changes; **slow** ones add a `runDataMigration` step for data backfills
|
||||
- **Workspace commands** iterate over all active/suspended workspaces for per-workspace upgrades
|
||||
- Commands use `@RegisteredInstanceCommand` and `@RegisteredWorkspaceCommand` decorators for automatic discovery
|
||||
- Include both `up` and `down` logic in instance commands
|
||||
- Never delete or rewrite committed instance command `up`/`down` logic
|
||||
- See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation
|
||||
- Always generate migrations when changing entity files
|
||||
- Migration names must be kebab-case (e.g. `add-agent-turn-evaluation`)
|
||||
- Include both `up` and `down` logic in migrations
|
||||
- Never delete or rewrite committed migrations
|
||||
|
||||
### Utility Helpers
|
||||
Use existing helpers from `twenty-shared` instead of manual type guards:
|
||||
@@ -181,7 +178,7 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li
|
||||
### Before Making Changes
|
||||
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
|
||||
2. Test changes with relevant test suites (prefer single-file test runs)
|
||||
3. Ensure instance commands are generated for entity changes (`database:migrate:generate`)
|
||||
3. Ensure database migrations are generated for entity changes
|
||||
4. Check that GraphQL schema changes are backward compatible
|
||||
5. Run `graphql:generate` after any GraphQL schema changes
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "1.22.0",
|
||||
"version": "0.9.0-canary.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"keywords": [],
|
||||
"keywords": [
|
||||
"twenty-app"
|
||||
],
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"twenty": "twenty",
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
|
||||
describe('App installation', () => {
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
tarball: true,
|
||||
onProgress: (message: string) => console.log(`[build] ${message}`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(
|
||||
`Build failed: ${buildResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
|
||||
const deployResult = await appDeploy({
|
||||
tarballPath: buildResult.data.tarballPath!,
|
||||
onProgress: (message: string) => console.log(`[deploy] ${message}`),
|
||||
});
|
||||
|
||||
if (!deployResult.success) {
|
||||
throw new Error(
|
||||
`Deploy failed: ${deployResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
|
||||
const installResult = await appInstall({ appPath: APP_PATH });
|
||||
|
||||
if (!installResult.success) {
|
||||
throw new Error(
|
||||
`Install failed: ${installResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should find the installed app in the applications list', async () => {
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const result = await metadataClient.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const installedApp = result.findManyApplications.find(
|
||||
(application: { universalIdentifier: string }) =>
|
||||
application.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(installedApp).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
|
||||
|
||||
function validateEnv(): { apiUrl: string; apiKey: string } {
|
||||
const apiUrl = process.env.TWENTY_API_URL;
|
||||
const apiKey = process.env.TWENTY_API_KEY;
|
||||
|
||||
if (!apiUrl || !apiKey) {
|
||||
throw new Error(
|
||||
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
|
||||
'Start a local server: yarn twenty server start\n' +
|
||||
'Or set them in vitest env config.',
|
||||
);
|
||||
}
|
||||
|
||||
return { apiUrl, apiKey };
|
||||
}
|
||||
|
||||
async function checkServer(apiUrl: string) {
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(`${apiUrl}/healthz`);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${apiUrl}. ` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function writeConfig(apiUrl: string, apiKey: string) {
|
||||
const payload = JSON.stringify(
|
||||
{
|
||||
remotes: {
|
||||
local: { apiUrl, apiKey, accessToken: apiKey },
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
fs.writeFileSync(path.join(CONFIG_DIR, 'config.test.json'), payload);
|
||||
}
|
||||
|
||||
export async function setup() {
|
||||
const { apiUrl, apiKey } = validateEnv();
|
||||
|
||||
await checkServer(apiUrl);
|
||||
|
||||
writeConfig(apiUrl, apiKey);
|
||||
|
||||
await appUninstall({ appPath: APP_PATH }).catch(() => {});
|
||||
|
||||
const result = await appDevOnce({
|
||||
appPath: APP_PATH,
|
||||
onProgress: (message: string) => console.log(`[dev] ${message}`),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`Dev sync failed: ${result.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function teardown() {
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('App installation', () => {
|
||||
it('should find the installed app in the applications list', async () => {
|
||||
const client = new MetadataApiClient();
|
||||
|
||||
const result = await client.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const app = result.findManyApplications.find(
|
||||
(a: { universalIdentifier: string }) =>
|
||||
a.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(app).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CoreApiClient', () => {
|
||||
it('should support CRUD on standard objects', async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const created = await client.mutation({
|
||||
createNote: {
|
||||
__args: { data: { title: 'Integration test note' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
expect(created.createNote.id).toBeDefined();
|
||||
|
||||
await client.mutation({
|
||||
destroyNote: {
|
||||
__args: { id: created.createNote.id },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,6 @@
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TWENTY_API_KEY =
|
||||
process.env.TWENTY_API_KEY ??
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc';
|
||||
|
||||
// Make env vars available to globalSetup (test.env only applies to workers)
|
||||
process.env.TWENTY_API_URL = TWENTY_API_URL;
|
||||
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
@@ -20,12 +11,14 @@ export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
fileParallelism: false,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
globalSetup: ['src/__tests__/global-setup.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL,
|
||||
TWENTY_API_KEY,
|
||||
TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020',
|
||||
TWENTY_API_KEY:
|
||||
process.env.TWENTY_API_KEY ??
|
||||
// Tim Apple (admin) access token for twenty-app-dev
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjQ5MDQ4ODE3MDR9.9S4wc0MOr5iczsomlFxZdOHD1IRDS4dnRSwNVNpctF4',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-client-sdk": "0.9.0",
|
||||
"twenty-sdk": "0.9.0"
|
||||
"twenty-client-sdk": "0.8.0-canary.5",
|
||||
"twenty-sdk": "0.8.0-canary.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import {
|
||||
definePostInstallLogicFunction,
|
||||
type InstallLogicFunctionPayload,
|
||||
} from 'twenty-sdk';
|
||||
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
|
||||
|
||||
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
||||
console.log(
|
||||
'Post install logic function executed successfully!',
|
||||
payload.previousVersion,
|
||||
);
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: '8c726dcc-1709-4eac-aa8b-f99960a9ec1b',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 30,
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
});
|
||||
|
||||
@@ -17,7 +17,8 @@ export default defineConfig({
|
||||
TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020',
|
||||
TWENTY_API_KEY:
|
||||
process.env.TWENTY_API_KEY ??
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc',
|
||||
// Tim Apple (admin) access token for twenty-app-dev
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjQ5MDQ4ODE3MDR9.9S4wc0MOr5iczsomlFxZdOHD1IRDS4dnRSwNVNpctF4',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,8 +19,8 @@
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-client-sdk": "0.9.0",
|
||||
"twenty-sdk": "0.9.0"
|
||||
"twenty-client-sdk": "0.8.0-canary.8",
|
||||
"twenty-sdk": "0.8.0-canary.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
|
||||
+2
-4
@@ -1,7 +1,7 @@
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application.config';
|
||||
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application.config';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
|
||||
@@ -44,9 +44,7 @@ describe('App installation', () => {
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${
|
||||
uninstallResult.error?.message ?? 'Unknown error'
|
||||
}`,
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
|
||||
|
||||
function validateEnv(): { apiUrl: string; apiKey: string } {
|
||||
const apiUrl = process.env.TWENTY_API_URL;
|
||||
const apiKey = process.env.TWENTY_API_KEY;
|
||||
|
||||
if (!apiUrl || !apiKey) {
|
||||
throw new Error(
|
||||
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
|
||||
'Start a local server: yarn twenty server start\n' +
|
||||
'Or set them in vitest env config.',
|
||||
);
|
||||
}
|
||||
|
||||
return { apiUrl, apiKey };
|
||||
}
|
||||
|
||||
async function checkServer(apiUrl: string) {
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(`${apiUrl}/healthz`);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${apiUrl}. ` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function writeConfig(apiUrl: string, apiKey: string) {
|
||||
const payload = JSON.stringify(
|
||||
{
|
||||
remotes: {
|
||||
local: { apiUrl, apiKey, accessToken: apiKey },
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
fs.writeFileSync(path.join(CONFIG_DIR, 'config.test.json'), payload);
|
||||
}
|
||||
|
||||
export async function setup() {
|
||||
const { apiUrl, apiKey } = validateEnv();
|
||||
|
||||
await checkServer(apiUrl);
|
||||
|
||||
writeConfig(apiUrl, apiKey);
|
||||
|
||||
await appUninstall({ appPath: APP_PATH }).catch(() => {});
|
||||
|
||||
const result = await appDevOnce({
|
||||
appPath: APP_PATH,
|
||||
onProgress: (message: string) => console.log(`[dev] ${message}`),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`Dev sync failed: ${result.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function teardown() {
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application.config';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('App installation', () => {
|
||||
it('should find the installed app in the applications list', async () => {
|
||||
const client = new MetadataApiClient();
|
||||
|
||||
const result = await client.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const app = result.findManyApplications.find(
|
||||
(a: { universalIdentifier: string }) =>
|
||||
a.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(app).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PostCard object', () => {
|
||||
it('should exist with expected fields and relations', async () => {
|
||||
const client = new MetadataApiClient();
|
||||
|
||||
const { objects } = await client.query({
|
||||
objects: {
|
||||
__args: {
|
||||
filter: { isCustom: { is: true } },
|
||||
paging: { first: 50 },
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
nameSingular: true,
|
||||
fields: {
|
||||
__args: { paging: { first: 500 } },
|
||||
edges: { node: { name: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const obj = objects.edges
|
||||
.map((e: { node: { nameSingular: string } }) => e.node)
|
||||
.find((n: { nameSingular: string }) => n.nameSingular === 'postCard');
|
||||
expect(obj).toBeDefined();
|
||||
|
||||
const names = obj!.fields.edges.map(
|
||||
(e: { node: { name: string } }) => e.node.name,
|
||||
);
|
||||
expect(names).toContain('name');
|
||||
expect(names).toContain('content');
|
||||
expect(names).toContain('status');
|
||||
expect(names).toContain('deliveredAt');
|
||||
expect(names).toContain('recipient');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
|
||||
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.test.json');
|
||||
|
||||
beforeAll(async () => {
|
||||
const apiUrl = process.env.TWENTY_API_URL!;
|
||||
const token = process.env.TWENTY_API_KEY!;
|
||||
|
||||
if (!apiUrl || !token) {
|
||||
throw new Error(
|
||||
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
|
||||
'Start a local server: yarn twenty server start\n' +
|
||||
'Or set them in vitest env config.',
|
||||
);
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(`${apiUrl}/healthz`);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${apiUrl}. ` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
|
||||
}
|
||||
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
CONFIG_PATH,
|
||||
JSON.stringify(
|
||||
{
|
||||
remotes: {
|
||||
local: { apiUrl, apiKey: token },
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
process.env.TWENTY_APP_ACCESS_TOKEN ??= token;
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { definePostInstallLogicFunction } from 'twenty-sdk';
|
||||
|
||||
const SEED_POST_CARDS = [
|
||||
{
|
||||
name: 'Greetings from Paris',
|
||||
content: 'Wish you were here! The Eiffel Tower is breathtaking.',
|
||||
},
|
||||
{
|
||||
name: 'Hello from Tokyo',
|
||||
content: 'Cherry blossoms are in full bloom. Sending love!',
|
||||
},
|
||||
];
|
||||
|
||||
const handler = async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
await client.mutation({
|
||||
createPostCards: {
|
||||
__args: { data: SEED_POST_CARDS as any },
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
console.log(`Seeded ${SEED_POST_CARDS.length} post cards on install.`);
|
||||
return {};
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: '852c6321-1563-4396-b7c5-9d370f3d30a9',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { definePreInstallLogicFunction } from 'twenty-sdk';
|
||||
|
||||
const handler = async (params: any) => {
|
||||
console.log(
|
||||
`Pre-install logic function executed successfully with params`,
|
||||
params,
|
||||
);
|
||||
return {};
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'bf27f558-4ec6-481f-b76e-1dbcd05aef1f',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before migrations to set up the application.',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { definePostInstallLogicFunction } from 'twenty-sdk';
|
||||
|
||||
const POST_CARDS_TO_SEED = [
|
||||
{
|
||||
name: 'Greetings from Paris',
|
||||
content:
|
||||
'Wish you were here! The Eiffel Tower looks even better in person. - Alex',
|
||||
},
|
||||
{
|
||||
name: 'Hello from Tokyo',
|
||||
content:
|
||||
'The cherry blossoms are amazing this time of year. See you soon! - Sam',
|
||||
},
|
||||
];
|
||||
|
||||
const handler = async (): Promise<{
|
||||
message: string;
|
||||
createdIds: string[];
|
||||
}> => {
|
||||
console.log('Seeding 2 post cards...');
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const createdIds: string[] = [];
|
||||
|
||||
for (const postCard of POST_CARDS_TO_SEED) {
|
||||
const { createPostCard } = await client.mutation({
|
||||
createPostCard: {
|
||||
__args: {
|
||||
data: {
|
||||
name: postCard.name,
|
||||
content: postCard.content,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!createPostCard?.id) {
|
||||
throw new Error(`Failed to create post card "${postCard.name}"`);
|
||||
}
|
||||
|
||||
createdIds.push(createPostCard.id);
|
||||
}
|
||||
|
||||
console.log('Seeding complete!');
|
||||
return {
|
||||
message: `Seeded ${createdIds.length} post cards`,
|
||||
createdIds,
|
||||
};
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: '9f3d8c21-b471-4a82-8e5c-6f3a7b8c9d01',
|
||||
name: 'seed-post-cards',
|
||||
description: 'Seeds the workspace with 2 sample post card records.',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
});
|
||||
@@ -1,15 +1,6 @@
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TWENTY_API_KEY =
|
||||
process.env.TWENTY_API_KEY ??
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc';
|
||||
|
||||
// Make env vars available to globalSetup (test.env only applies to workers)
|
||||
process.env.TWENTY_API_URL = TWENTY_API_URL;
|
||||
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
@@ -20,12 +11,14 @@ export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
fileParallelism: false,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
globalSetup: ['src/__tests__/global-setup.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL,
|
||||
TWENTY_API_KEY,
|
||||
TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020',
|
||||
TWENTY_API_KEY:
|
||||
process.env.TWENTY_API_KEY ??
|
||||
// Tim Apple (admin) access token for twenty-app-dev
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjQ5MDQ4ODE3MDR9.9S4wc0MOr5iczsomlFxZdOHD1IRDS4dnRSwNVNpctF4',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3317,8 +3317,8 @@ __metadata:
|
||||
oxlint: "npm:^0.16.0"
|
||||
react: "npm:^19.0.0"
|
||||
react-dom: "npm:^19.0.0"
|
||||
twenty-client-sdk: "npm:0.9.0"
|
||||
twenty-sdk: "npm:0.9.0"
|
||||
twenty-client-sdk: "npm:0.8.0-canary.8"
|
||||
twenty-sdk: "npm:0.8.0-canary.8"
|
||||
typescript: "npm:^5.9.3"
|
||||
vite-tsconfig-paths: "npm:^4.2.1"
|
||||
vitest: "npm:^3.1.1"
|
||||
@@ -4059,21 +4059,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"twenty-client-sdk@npm:0.9.0":
|
||||
version: 0.9.0
|
||||
resolution: "twenty-client-sdk@npm:0.9.0"
|
||||
"twenty-client-sdk@npm:0.8.0-canary.8":
|
||||
version: 0.8.0-canary.8
|
||||
resolution: "twenty-client-sdk@npm:0.8.0-canary.8"
|
||||
dependencies:
|
||||
"@genql/cli": "npm:^3.0.3"
|
||||
"@genql/runtime": "npm:^2.10.0"
|
||||
esbuild: "npm:^0.25.0"
|
||||
graphql: "npm:^16.8.1"
|
||||
checksum: 10c0/4b42a6622a9852fc3eca50c1131b116c5602af006ee5f1d3b4f1e97721bbc8ef5c2f3b60d9dee3ca9ca8c3c7ad4b292a7b55007b779b4c042997dbc29940ba54
|
||||
checksum: 10c0/acb5bf952a9729811235a3474ccb4db82630c53a2caed03176bfb4f442576ee2ef7e625886f63714e2534f7ec83b03d83e4c0b1170a0eb816c531acfc2c98010
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"twenty-sdk@npm:0.9.0":
|
||||
version: 0.9.0
|
||||
resolution: "twenty-sdk@npm:0.9.0"
|
||||
"twenty-sdk@npm:0.8.0-canary.8":
|
||||
version: 0.8.0-canary.8
|
||||
resolution: "twenty-sdk@npm:0.8.0-canary.8"
|
||||
dependencies:
|
||||
"@genql/cli": "npm:^3.0.3"
|
||||
"@genql/runtime": "npm:^2.10.0"
|
||||
@@ -4093,7 +4093,7 @@ __metadata:
|
||||
react: "npm:^19.0.0"
|
||||
react-dom: "npm:^19.0.0"
|
||||
tinyglobby: "npm:^0.2.15"
|
||||
twenty-client-sdk: "npm:0.9.0"
|
||||
twenty-client-sdk: "npm:0.8.0-canary.8"
|
||||
typescript: "npm:^5.9.2"
|
||||
uuid: "npm:^13.0.0"
|
||||
vite: "npm:^7.0.0"
|
||||
@@ -4101,7 +4101,7 @@ __metadata:
|
||||
zod: "npm:^4.1.11"
|
||||
bin:
|
||||
twenty: dist/cli.cjs
|
||||
checksum: 10c0/27f93e5edac3265f819abacc853598435718020258a95d55518562e086e42daabae784964b42005d8e4ff30d1d6f13a12a38c656e18c60bad98da3f579a8e1c3
|
||||
checksum: 10c0/a22cf071f41c19d769e68a995912a0bda0f087bd9a295534bf9e545544f6dfc84f284a477f9011fc135f2a44de9372b977a66de06454e760822119e76921ea69
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "latest",
|
||||
"twenty-client-sdk": "latest"
|
||||
"twenty-sdk": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "latest",
|
||||
"twenty-client-sdk": "latest"
|
||||
"twenty-sdk": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "latest",
|
||||
"twenty-client-sdk": "latest"
|
||||
"twenty-sdk": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
name: CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TWENTY_DEPLOY_URL: http://localhost:3000
|
||||
|
||||
concurrency:
|
||||
group: cd-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy-and-install:
|
||||
if: >-
|
||||
github.event_name == 'push' ||
|
||||
(github.event_name == 'pull_request' && github.event.label.name == 'deploy')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Deploy
|
||||
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
|
||||
with:
|
||||
api-url: ${{ env.TWENTY_DEPLOY_URL }}
|
||||
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
|
||||
|
||||
- name: Install
|
||||
uses: twentyhq/twenty/.github/actions/install-twenty-app@main
|
||||
with:
|
||||
api-url: ${{ env.TWENTY_DEPLOY_URL }}
|
||||
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
|
||||
@@ -1,48 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty test instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
|
||||
@@ -35,4 +35,3 @@ yarn-error.log*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
*.d.ts
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
24.5.0
|
||||
@@ -1,19 +1,40 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript"],
|
||||
"plugins": ["typescript", "import", "unicorn"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist"],
|
||||
"ignorePatterns": ["node_modules"],
|
||||
"rules": {
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
"no-console": ["warn", { "allow": ["group", "groupCollapsed", "groupEnd"] }],
|
||||
"no-control-regex": "off",
|
||||
"no-debugger": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-undef": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-redeclare": "off",
|
||||
|
||||
"typescript/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"typescript/no-explicit-any": "off"
|
||||
"import/no-duplicates": "error",
|
||||
|
||||
"typescript/no-redeclare": "error",
|
||||
"typescript/ban-ts-comment": "error",
|
||||
"typescript/consistent-type-imports": ["error", {
|
||||
"prefer": "type-imports",
|
||||
"fixStyle": "inline-type-imports"
|
||||
}],
|
||||
"typescript/explicit-function-return-type": "off",
|
||||
"typescript/explicit-module-boundary-types": "off",
|
||||
"typescript/no-empty-object-type": ["error", {
|
||||
"allowInterfaces": "with-single-extends"
|
||||
}],
|
||||
"typescript/no-empty-function": "off",
|
||||
"typescript/no-explicit-any": "off",
|
||||
"typescript/no-unused-vars": ["warn", {
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
yarnPath: .yarn/releases/yarn-4.9.2.cjs
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
enableTransparentWorkspaces: false
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
|
||||
|
||||
## UUID requirement
|
||||
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
@@ -1,14 +0,0 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
|
||||
|
||||
## UUID requirement
|
||||
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
@@ -1,14 +0,0 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
|
||||
|
||||
## UUID requirement
|
||||
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
@@ -1,11 +1,44 @@
|
||||
This is a [Twenty](https://twenty.com) application bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
|
||||
# Self Hosting
|
||||
|
||||
## Getting Started
|
||||
Used to manage billing and telemetry of self-hosted instances
|
||||
|
||||
Run `yarn twenty help` to list all available commands.
|
||||
## Features
|
||||
|
||||
## Learn More
|
||||
### Telemetry Webhook
|
||||
|
||||
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started)
|
||||
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
|
||||
- [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
Receives user signup telemetry events from self-hosted Twenty instances and creates/updates selfHostingUser records.
|
||||
|
||||
**Endpoint:** `POST /webhook/telemetry`
|
||||
|
||||
**Payload Structure:**
|
||||
```json
|
||||
{
|
||||
"action": "user_signup",
|
||||
"timestamp": "2025-11-21T...",
|
||||
"version": "1",
|
||||
"payload": {
|
||||
"userId": "uuid",
|
||||
"workspaceId": "uuid",
|
||||
"payload": {
|
||||
"events": [
|
||||
{
|
||||
"userEmail": "user@example.com",
|
||||
"userId": "uuid",
|
||||
"userFirstName": "John",
|
||||
"userLastName": "Doe",
|
||||
"locale": "en",
|
||||
"serverUrl": "https://self-hosted.example.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Self hosting user created/updated: uuid"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "self-hosting",
|
||||
"version": "1.0.0",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
@@ -11,22 +11,13 @@
|
||||
"scripts": {
|
||||
"twenty": "twenty",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-client-sdk": "1.22.0-canary.6",
|
||||
"twenty-sdk": "1.22.0-canary.6"
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^19.0.0",
|
||||
"oxlint": "^0.16.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
"twenty-sdk": "0.6.2"
|
||||
},
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/appManifest.schema.json",
|
||||
"universalIdentifier": "a7070f46-3158-4b40-828f-8e6b1febc233"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
|
||||
|
||||
export const APPLICATION_UNIVERSAL_IDENTIFIER =
|
||||
'94f7db30-59e5-4b09-a5fe-64cd3d4a65b0';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '94f7db30-59e5-4b09-a5fe-64cd3d4a65b0',
|
||||
displayName: 'Self Hosting',
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import {
|
||||
type ObjectRecordUpdateEvent,
|
||||
} from 'twenty-sdk';
|
||||
import { SELF_HOSTING_USER_NAME_SINGULAR } from 'src/objects/selfHostingUser.object';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
|
||||
type SelfHostingUser = {
|
||||
id: string;
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
import { type TelemetryEvent } from 'src/logic-functions/types/telemetry-event.type';
|
||||
|
||||
export const main = async (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.const
|
||||
export default defineRole({
|
||||
universalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.roles.defaultRole.universalIdentifier,
|
||||
label: 'Self hosting default role',
|
||||
label: 'default role',
|
||||
description: 'Add a description for your role',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
|
||||
@@ -27,16 +27,5 @@
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.integration-test.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["vitest/globals", "node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: process.env.TWENTY_API_URL ?? 'http://localhost:2020',
|
||||
TWENTY_API_KEY:
|
||||
process.env.TWENTY_API_KEY ??
|
||||
// Tim Apple (admin) access token for twenty-app-dev
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjQ5MDQ4ODE3MDR9.9S4wc0MOr5iczsomlFxZdOHD1IRDS4dnRSwNVNpctF4',
|
||||
},
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,42 +0,0 @@
|
||||
name: CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TWENTY_DEPLOY_URL: http://localhost:3000
|
||||
|
||||
concurrency:
|
||||
group: cd-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy-and-install:
|
||||
if: >-
|
||||
github.event_name == 'push' ||
|
||||
(github.event_name == 'pull_request' && github.event.label.name == 'deploy')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Deploy
|
||||
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
|
||||
with:
|
||||
api-url: ${{ env.TWENTY_DEPLOY_URL }}
|
||||
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
|
||||
|
||||
- name: Install
|
||||
uses: twentyhq/twenty/.github/actions/install-twenty-app@main
|
||||
with:
|
||||
api-url: ${{ env.TWENTY_DEPLOY_URL }}
|
||||
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
|
||||
@@ -1,48 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty test instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
|
||||
@@ -1,38 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn
|
||||
|
||||
# codegen
|
||||
generated
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# dev
|
||||
/dist/
|
||||
|
||||
.twenty
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
*.d.ts
|
||||
@@ -1 +0,0 @@
|
||||
24.5.0
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist"],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
|
||||
"typescript/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"typescript/no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
|
||||
|
||||
## UUID requirement
|
||||
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
@@ -1,14 +0,0 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
|
||||
|
||||
## UUID requirement
|
||||
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
@@ -1,14 +0,0 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
|
||||
|
||||
## UUID requirement
|
||||
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
@@ -1,122 +0,0 @@
|
||||
This is a [Twenty](https://twenty.com) application bootstrapped with [`create-twenty-app`](https://www.npmjs.com/package/create-twenty-app).
|
||||
|
||||
## Overview
|
||||
|
||||
**Twenty for Twenty** is the official internal Twenty app. It is organized into modules, each integrating a third-party service with Twenty.
|
||||
|
||||
### Resend module (`src/modules/resend/`)
|
||||
|
||||
Two-way sync between Twenty and the [Resend](https://resend.com) email platform. The module syncs contacts, segments, templates, broadcasts, and emails.
|
||||
|
||||
**Inbound (Resend -> Twenty):**
|
||||
|
||||
- A cron job runs every 5 minutes to pull all entities from the Resend API
|
||||
- A webhook endpoint receives real-time events for contacts and emails
|
||||
|
||||
**Outbound (Twenty -> Resend):**
|
||||
|
||||
- Database event triggers push contact and segment changes back to Resend when records are created, updated, or deleted in Twenty
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Install and run the app
|
||||
|
||||
```bash
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
This registers the app with your local Twenty instance at `http://localhost:3000/settings/applications`.
|
||||
|
||||
### 2. Configure app variables
|
||||
|
||||
In Twenty, go to **Settings > Applications > Twenty for Twenty** and set:
|
||||
|
||||
- **RESEND_API_KEY** -- Your Resend API key. Create one at https://resend.com/api-keys (full access recommended).
|
||||
- **RESEND_WEBHOOK_SECRET** -- The signing secret for verifying inbound webhooks (see "Webhook setup" below).
|
||||
|
||||
### 3. Webhook setup
|
||||
|
||||
The app exposes an HTTP endpoint at `/s/webhook/resend` that receives Resend webhook events. To connect it:
|
||||
|
||||
1. Go to https://resend.com/webhooks
|
||||
2. Click **Add webhook**
|
||||
3. Set the **Endpoint URL** to your Twenty server's public URL + `/s/webhook/resend` (e.g. `https://your-domain.com/s/webhook/resend`)
|
||||
4. Set **Events types** to **All Events**
|
||||
5. Click **Add**
|
||||
6. Copy the **signing secret** Resend displays and paste it into the `RESEND_WEBHOOK_SECRET` app variable in Twenty
|
||||
|
||||
The webhook handles:
|
||||
|
||||
- **Contact events** (`contact.created`, `contact.updated`, `contact.deleted`) -- upserts/deletes Resend contact records in Twenty
|
||||
- **Email events** (`email.sent`, `email.delivered`, `email.bounced`, `email.opened`, `email.clicked`, etc.) -- updates delivery status on Resend email records in real-time
|
||||
- **Domain events** -- logged and skipped (no domain object in the app yet)
|
||||
|
||||
### 4. Testing webhooks locally
|
||||
|
||||
Install the [Resend CLI](https://resend.com/docs/resend-cli):
|
||||
|
||||
```bash
|
||||
brew install resend/cli/resend
|
||||
```
|
||||
|
||||
Or via npm if Homebrew has issues:
|
||||
|
||||
```bash
|
||||
npm install -g resend-cli
|
||||
```
|
||||
|
||||
Authenticate:
|
||||
|
||||
```bash
|
||||
resend login
|
||||
```
|
||||
|
||||
Start the webhook listener with forwarding to your local Twenty server:
|
||||
|
||||
```bash
|
||||
resend webhooks listen --forward-to http://localhost:3000/s/webhook/resend
|
||||
```
|
||||
|
||||
The CLI will:
|
||||
|
||||
1. Create a public tunnel automatically
|
||||
2. Register a temporary webhook in Resend pointing to that tunnel
|
||||
3. Forward incoming events (with Svix signature headers) to your local Twenty server
|
||||
4. Display events in the terminal as they arrive
|
||||
5. Clean up the temporary webhook when you press Ctrl+C
|
||||
|
||||
To trigger test events, create or update a contact in the [Resend dashboard](https://resend.com/contacts), or send a test email.
|
||||
|
||||
## Sync behavior
|
||||
|
||||
### Inbound sync
|
||||
|
||||
| Source | Mechanism | Entities |
|
||||
|---|---|---|
|
||||
| Cron (every 5 min) | Polls Resend API, upserts into Twenty | Contacts, segments, templates, broadcasts, emails |
|
||||
| Webhook (real-time) | Receives Resend events via HTTP | Contacts, emails |
|
||||
|
||||
### Outbound sync
|
||||
|
||||
| Twenty action | Resend API call |
|
||||
|---|---|
|
||||
| Create contact | `contacts.create()` -- writes `resendId` back to Twenty |
|
||||
| Update contact (name, email, unsubscribed) | `contacts.update()` |
|
||||
| Delete contact | `contacts.remove()` |
|
||||
| Create segment | `segments.create()` -- writes `resendId` back to Twenty |
|
||||
| Delete segment | `segments.remove()` |
|
||||
|
||||
### Loop prevention
|
||||
|
||||
A `lastSyncedFromResend` field on contact, segment, and email records tracks when data came from Resend. Outbound triggers skip processing when this field is part of the update, preventing infinite echo loops between inbound and outbound sync.
|
||||
|
||||
## Commands
|
||||
|
||||
Run `yarn twenty help` to list all available commands.
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started)
|
||||
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
|
||||
- [Resend API documentation](https://resend.com/docs)
|
||||
- [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "twenty-for-twenty",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"keywords": [],
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"twenty": "twenty",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:unit": "vitest run --config vitest.unit.config.ts",
|
||||
"test:unit:watch": "vitest --config vitest.unit.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"resend": "^6.12.0",
|
||||
"twenty-client-sdk": "1.22.0",
|
||||
"twenty-sdk": "1.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^19.0.0",
|
||||
"oxlint": "^0.16.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="1800" height="1800" viewBox="0 0 1800 1800" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1000.46 450C1174.77 450 1278.43 553.669 1278.43 691.282C1278.43 828.896 1174.77 932.563 1000.46 932.563H912.382L1350 1350H1040.82L707.794 1033.48C683.944 1011.47 672.936 985.781 672.935 963.765C672.935 932.572 694.959 905.049 737.161 893.122L908.712 847.244C973.85 829.812 1018.81 779.353 1018.81 713.298C1018.8 632.567 952.745 585.78 871.095 585.78H450V450H1000.46Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 504 B |
@@ -1,87 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
|
||||
|
||||
function validateEnv(): { apiUrl: string; apiKey: string } {
|
||||
const apiUrl = process.env.TWENTY_API_URL;
|
||||
const apiKey = process.env.TWENTY_API_KEY;
|
||||
|
||||
if (!apiUrl || !apiKey) {
|
||||
throw new Error(
|
||||
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
|
||||
'Start a local server: yarn twenty server start\n' +
|
||||
'Or set them in vitest env config.',
|
||||
);
|
||||
}
|
||||
|
||||
return { apiUrl, apiKey };
|
||||
}
|
||||
|
||||
async function checkServer(apiUrl: string) {
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(`${apiUrl}/healthz`);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${apiUrl}. ` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function writeConfig(apiUrl: string, apiKey: string) {
|
||||
const payload = JSON.stringify(
|
||||
{
|
||||
remotes: {
|
||||
local: { apiUrl, apiKey, accessToken: apiKey },
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
fs.writeFileSync(path.join(CONFIG_DIR, 'config.test.json'), payload);
|
||||
}
|
||||
|
||||
export async function setup() {
|
||||
const { apiUrl, apiKey } = validateEnv();
|
||||
|
||||
await checkServer(apiUrl);
|
||||
|
||||
writeConfig(apiUrl, apiKey);
|
||||
|
||||
await appUninstall({ appPath: APP_PATH }).catch(() => {});
|
||||
|
||||
const result = await appDevOnce({
|
||||
appPath: APP_PATH,
|
||||
onProgress: (message: string) => console.log(`[dev] ${message}`),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`Dev sync failed: ${result.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function teardown() {
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('App installation', () => {
|
||||
it('should find the installed app in the applications list', async () => {
|
||||
const client = new MetadataApiClient();
|
||||
|
||||
const result = await client.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const app = result.findManyApplications.find(
|
||||
(a: { universalIdentifier: string }) =>
|
||||
a.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(app).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CoreApiClient', () => {
|
||||
it('should support CRUD on standard objects', async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const created = await client.mutation({
|
||||
createNote: {
|
||||
__args: { data: { title: 'Integration test note' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
expect(created.createNote.id).toBeDefined();
|
||||
|
||||
await client.mutation({
|
||||
destroyNote: {
|
||||
__args: { id: created.createNote.id },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
|
||||
import {
|
||||
APP_DESCRIPTION,
|
||||
APP_DISPLAY_NAME,
|
||||
APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
import {
|
||||
RESEND_API_KEY_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_WEBHOOK_SECRET_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
displayName: APP_DISPLAY_NAME,
|
||||
description: APP_DESCRIPTION,
|
||||
logoUrl: 'public/resend-icon-black.svg',
|
||||
applicationVariables: {
|
||||
RESEND_API_KEY: {
|
||||
universalIdentifier: RESEND_API_KEY_UNIVERSAL_IDENTIFIER,
|
||||
description: 'API key for the Resend service',
|
||||
isSecret: true,
|
||||
},
|
||||
RESEND_WEBHOOK_SECRET: {
|
||||
universalIdentifier: RESEND_WEBHOOK_SECRET_UNIVERSAL_IDENTIFIER,
|
||||
description: 'Signing secret for verifying Resend webhook payloads',
|
||||
isSecret: true,
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
export const APP_DISPLAY_NAME = 'Twenty for Twenty';
|
||||
export const APP_DESCRIPTION =
|
||||
'The official Twenty internal app – modules for Resend and more';
|
||||
export const APPLICATION_UNIVERSAL_IDENTIFIER = '9426c4a3-7da4-4f61-bdb7-01e1276478b8';
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'b115921d-7ca3-4a1f-94e3-7119174fef78';
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defineRole } from 'twenty-sdk';
|
||||
|
||||
import {
|
||||
APP_DISPLAY_NAME,
|
||||
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: `${APP_DISPLAY_NAME} default function role`,
|
||||
description: `${APP_DISPLAY_NAME} default function role`,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: false,
|
||||
});
|
||||
-329
@@ -1,329 +0,0 @@
|
||||
// App-level
|
||||
export const RESEND_API_KEY_UNIVERSAL_IDENTIFIER =
|
||||
'e5828892-33d5-4532-b796-551df48a07c0';
|
||||
export const RESEND_WEBHOOK_SECRET_UNIVERSAL_IDENTIFIER =
|
||||
'b291b241-bd84-4661-8e79-3dc7a63371dd';
|
||||
|
||||
// Logic functions
|
||||
export const SYNC_RESEND_DATA_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
|
||||
'7a3c841f-509e-46f0-b2f1-fb942b716ee3';
|
||||
export const RESEND_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
|
||||
'049b7227-3e8b-444b-84aa-939a7e4ca440';
|
||||
export const ON_RESEND_CONTACT_CREATED_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
|
||||
'656b85b4-71d0-477b-9741-4967d8d88ac9';
|
||||
export const ON_RESEND_CONTACT_UPDATED_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
|
||||
'7b770cc2-6d31-4f1b-a7db-48d44cf6109b';
|
||||
export const ON_RESEND_CONTACT_DELETED_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
|
||||
'7a2341e7-d96a-4ba1-b41d-699c73d61081';
|
||||
export const ON_RESEND_SEGMENT_CREATED_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
|
||||
'ac5b424a-f51d-46c8-a95a-42589fb81676';
|
||||
export const ON_RESEND_SEGMENT_DELETED_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
|
||||
'd5e5b6e1-e0d9-45f0-b3d9-6b96417e4ed0';
|
||||
|
||||
// Commands
|
||||
export const SYNC_RESEND_DATA_COMMAND_UNIVERSAL_IDENTIFIER =
|
||||
'70c1c7bc-b3f1-491a-90c8-2616facc7a9c';
|
||||
|
||||
// Front components
|
||||
export const SYNC_RESEND_DATA_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
|
||||
'96e073b8-e331-40d1-9ec0-137bc921f486';
|
||||
export const EMAIL_HTML_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
|
||||
'7df0713e-3c16-4cbc-a25f-08cead363941';
|
||||
export const TEMPLATE_HTML_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
|
||||
'58d79064-1e2a-4500-b8e7-c023cd9835fe';
|
||||
|
||||
// Objects
|
||||
export const RESEND_EMAIL_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
'd59c00df-9715-46b1-bacd-580681435cef';
|
||||
export const RESEND_SEGMENT_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
'f06283b0-c7f9-4267-86de-e489f816cca1';
|
||||
export const RESEND_CONTACT_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
'cb91a26f-131b-4db4-916b-7a308fcc29d7';
|
||||
export const RESEND_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
'85ddb31f-0d1c-4619-bbaf-3d208c1b9fea';
|
||||
export const RESEND_BROADCAST_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
'bebc114f-a8f5-455d-8c6f-e33f20f66967';
|
||||
|
||||
// Object fields - Resend email
|
||||
export const SUBJECT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'9e52c08b-d619-445a-b70b-121dc7676ff3';
|
||||
export const FROM_ADDRESS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'c663f57e-0fe3-4066-91df-9eb001bab03a';
|
||||
export const TO_ADDRESSES_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'613e0400-709d-496e-9be4-b6eab8282d2e';
|
||||
export const HTML_BODY_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd14c29e4-c971-47d3-a1f1-8f459b8d8719';
|
||||
export const TEXT_BODY_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'9d6edd43-903b-4e57-8bd0-8bbd9e914c30';
|
||||
export const CC_ADDRESSES_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'eff53046-039e-444e-9142-33d7cc354ad6';
|
||||
export const BCC_ADDRESSES_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'3db64fe7-99e9-4d44-81be-e0a55d32b211';
|
||||
export const REPLY_TO_ADDRESSES_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'acbea701-ca16-4db2-960f-7d8c8493e42d';
|
||||
export const LAST_EVENT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'c24331f3-43da-4143-9763-53ae01205300';
|
||||
export const EMAIL_RESEND_ID_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'374e4e3a-dc13-4159-813e-e5da789a97f0';
|
||||
export const EMAIL_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'732e6009-67f7-4880-8161-a4310f4df690';
|
||||
export const SCHEDULED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'3d1b8deb-f102-4c1b-929e-ed7b2647e64b';
|
||||
export const TAGS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd6406a2c-a8b6-416e-b351-e1e5ddb3d5fe';
|
||||
export const EMAIL_LAST_SYNCED_FROM_RESEND_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'1b841d96-4318-48f5-aa0d-184d19e9af55';
|
||||
|
||||
// Object fields - Resend segment
|
||||
export const SEGMENT_NAME_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'92aad4eb-bfc7-4c3d-aa0a-fad5cf9cbda1';
|
||||
export const SEGMENT_RESEND_ID_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'5bce2a58-9eae-4903-b599-3a602e759373';
|
||||
export const SEGMENT_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'4757fafa-3032-4f69-ba09-86d968239a8f';
|
||||
export const SEGMENT_LAST_SYNCED_FROM_RESEND_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'8001f093-09c0-4a22-b7a3-9b1dcee47ce2';
|
||||
|
||||
// Object fields - Resend contact
|
||||
export const CONTACT_EMAIL_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'62ce21e5-a015-4f26-8c7a-3c141b6d7064';
|
||||
export const NAME_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'e0fb9280-4588-4c2b-8996-bbd4c1d33f54';
|
||||
export const UNSUBSCRIBED_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'41403b6f-b91e-4eb6-8875-8b5c21b0a6d3';
|
||||
export const CONTACT_RESEND_ID_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'c84c8703-33e1-4acb-8a7f-1d62647166d5';
|
||||
export const CONTACT_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'040bf210-36cf-49cb-8d83-0da9b864c900';
|
||||
export const CONTACT_LAST_SYNCED_FROM_RESEND_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd1874e6a-db94-4308-afc6-aeb4dc3a9eb2';
|
||||
|
||||
// Object fields - Resend template
|
||||
export const TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'a045319f-483d-4625-86cd-653111dc8ac3';
|
||||
export const TEMPLATE_ALIAS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'850a9d4a-df38-4af0-ba69-aaef090cafef';
|
||||
export const TEMPLATE_STATUS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'f98b84c0-c8ef-4f9e-a72a-a53e2ef60fea';
|
||||
export const TEMPLATE_FROM_ADDRESS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'33a80d6c-e6ee-490e-b5ac-87b557bcdf50';
|
||||
export const TEMPLATE_SUBJECT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'3aff5a64-49fa-450c-b85d-4d4f210f6433';
|
||||
export const TEMPLATE_REPLY_TO_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'b05c1a87-4cea-4e01-8e6a-eff5929de149';
|
||||
export const TEMPLATE_HTML_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'9ced95c8-345d-438d-97dc-f52b99f4a9c3';
|
||||
export const TEMPLATE_TEXT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd7614d4c-ed00-4961-8765-7c8b0c9a320b';
|
||||
export const TEMPLATE_RESEND_ID_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'3883be35-ab0e-4bed-bc59-131683b9a0b2';
|
||||
export const TEMPLATE_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'72939761-7a57-4448-997a-68da6b4f60db';
|
||||
export const TEMPLATE_UPDATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'cfe6a645-ee89-4117-8ace-fb9623e726cc';
|
||||
export const TEMPLATE_PUBLISHED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'669a3ebb-d60b-41a3-81e9-53ab8b18f2b1';
|
||||
|
||||
// Object fields - Resend broadcast
|
||||
export const BROADCAST_NAME_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'e3222f79-751d-4c9e-b33d-c5f2e6ee8304';
|
||||
export const BROADCAST_SUBJECT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'51025e6b-375a-45a0-89eb-773314702073';
|
||||
export const BROADCAST_FROM_ADDRESS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'fa189da3-3443-4984-b637-7fae9b30e2c5';
|
||||
export const BROADCAST_REPLY_TO_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'8674b4e8-1881-4001-a9df-8e9258b59d50';
|
||||
export const PREVIEW_TEXT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'b8a008aa-1a44-4edb-b472-f392af635867';
|
||||
export const BROADCAST_STATUS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'394135a4-6488-4ec3-97bc-d0514921ded8';
|
||||
export const BROADCAST_RESEND_ID_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'ee40a63f-6d32-496a-9f4d-1abfba386909';
|
||||
export const BROADCAST_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'01855a84-450b-4de9-b172-e51555724370';
|
||||
export const BROADCAST_SCHEDULED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'abbb4379-d0a5-432a-8c34-0e940242d687';
|
||||
export const BROADCAST_SENT_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'e1e281aa-7ba7-47a0-951d-0f6150a63099';
|
||||
|
||||
// Relation fields
|
||||
export const SEGMENT_ON_RESEND_BROADCAST_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'8b335824-b19d-486e-b865-5761c795c971';
|
||||
export const RESEND_BROADCASTS_ON_SEGMENT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'784601f0-e892-4164-90db-6903ab062c7e';
|
||||
export const PERSON_ON_RESEND_CONTACT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'90353fa9-10fd-4cc8-b7b0-ef9a5d17ff8e';
|
||||
export const RESEND_CONTACTS_ON_PERSON_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'134222c5-7760-4fd1-b89d-8a956f3068c5';
|
||||
export const SEGMENT_ON_RESEND_CONTACT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'5fbd9063-fb4e-4584-820b-27918b6f95f0';
|
||||
export const BROADCAST_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'4cd54c84-1e3e-4d3f-a35d-b32a6e8e1137';
|
||||
export const RESEND_EMAILS_ON_BROADCAST_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'0b346627-4797-4cff-8a7c-1dc8f4580d6f';
|
||||
export const CONTACT_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'46ffbf1c-0c25-4995-a209-291626b6fd2d';
|
||||
export const RESEND_EMAILS_ON_PERSON_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'c6142228-e0e4-4143-9942-df4fce3ef231';
|
||||
export const RESEND_CONTACTS_ON_SEGMENT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'ba14727c-19eb-4b08-844c-88bf33b8267d';
|
||||
export const PERSON_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'cb08e862-8114-480e-986b-8f50fe49de41';
|
||||
export const RESEND_EMAILS_ON_CONTACT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd8b69019-ca10-4599-b099-ecfc891d6438';
|
||||
|
||||
// Views
|
||||
export const RESEND_BROADCAST_VIEW_UNIVERSAL_IDENTIFIER =
|
||||
'9875e352-4dd7-4296-9291-5de5864594b8';
|
||||
export const RESEND_TEMPLATE_VIEW_UNIVERSAL_IDENTIFIER =
|
||||
'c4a00e17-cec7-44f6-85c6-5826a0db8923';
|
||||
export const RESEND_SEGMENT_VIEW_UNIVERSAL_IDENTIFIER =
|
||||
'30683084-d0d4-4d25-bb5f-c6bcff9fc92a';
|
||||
export const RESEND_CONTACT_VIEW_UNIVERSAL_IDENTIFIER =
|
||||
'3d710924-5f47-4ac7-ba5e-28d3be9ee004';
|
||||
export const RESEND_EMAIL_VIEW_UNIVERSAL_IDENTIFIER =
|
||||
'43571c5b-f71d-47bf-95b3-8741b2201315';
|
||||
|
||||
// View fields - Resend broadcast view
|
||||
export const RESEND_BROADCAST_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'7d2bd63b-d609-441e-97b7-8d72e1f64fcb';
|
||||
export const RESEND_BROADCAST_VIEW_SUBJECT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'0cb2b32f-a7ca-4c6e-91af-705be37416dd';
|
||||
export const RESEND_BROADCAST_VIEW_FROM_ADDRESS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'24261a77-9ddd-4fee-bcef-10308b6e438e';
|
||||
export const RESEND_BROADCAST_VIEW_STATUS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'ba957060-3879-4812-98ac-0199732c79d9';
|
||||
export const RESEND_BROADCAST_VIEW_SENT_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'7e78132b-7b33-468e-962d-d7bb1b3025d4';
|
||||
export const RESEND_BROADCAST_VIEW_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'fdcf945c-5d0f-42f2-94e6-765d5216bd2f';
|
||||
export const RESEND_BROADCAST_VIEW_SEGMENT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'4d7eb630-9f44-4090-a887-7fb08e0c49ed';
|
||||
export const RESEND_BROADCAST_VIEW_EMAILS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'e1726752-fed7-4e9b-b395-e47b60f3d56d';
|
||||
|
||||
// View fields - Resend template view
|
||||
export const RESEND_TEMPLATE_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'01464797-cd0c-4686-b4fd-8e7f9d2e81d4';
|
||||
export const RESEND_TEMPLATE_VIEW_SUBJECT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'c9c823c4-27f2-4805-a6ce-e22e97c5ac16';
|
||||
export const RESEND_TEMPLATE_VIEW_FROM_ADDRESS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'36a274f8-4273-4b16-8a66-99c652ca308d';
|
||||
export const RESEND_TEMPLATE_VIEW_STATUS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'0474cc6b-3e28-4fb6-b0ba-3b22004eb9c4';
|
||||
export const RESEND_TEMPLATE_VIEW_UPDATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'6bc34358-17b2-40c6-a0bc-3320ac972736';
|
||||
export const RESEND_TEMPLATE_VIEW_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'e3e90309-715c-4962-95d3-ddc46124fc93';
|
||||
|
||||
// View fields - Resend segment view
|
||||
export const RESEND_SEGMENT_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'dc878169-b670-4aff-90b4-869849ec063e';
|
||||
export const RESEND_SEGMENT_VIEW_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'4898ae92-b94e-4bbf-850d-76011613fd64';
|
||||
export const RESEND_SEGMENT_VIEW_CONTACTS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'cd223c54-d968-4830-b84f-ebefe8595842';
|
||||
export const RESEND_SEGMENT_VIEW_BROADCASTS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'0adacbc6-f355-49bb-9350-b7202cf28b8c';
|
||||
|
||||
// View fields - Resend contact view
|
||||
export const RESEND_CONTACT_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'f589ef0c-0f8b-4895-8d84-bd3b743f925f';
|
||||
export const RESEND_CONTACT_VIEW_EMAIL_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'3af5988f-de6b-46de-996b-439bd5acd51c';
|
||||
export const RESEND_CONTACT_VIEW_UNSUBSCRIBED_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'efa38cf1-afcf-461a-9780-6d916e02256b';
|
||||
export const RESEND_CONTACT_VIEW_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'a48545bb-8cf1-4836-82fa-d0c7bc314a1b';
|
||||
export const RESEND_CONTACT_VIEW_PERSON_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'9fc1ab4b-fc2d-4c33-9de5-3cff7b0ea5ec';
|
||||
export const RESEND_CONTACT_VIEW_SEGMENT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'54949f7e-0454-45a5-9e30-8f16f75adeaa';
|
||||
export const RESEND_CONTACT_VIEW_EMAILS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'fbbe207d-dbda-4bda-935c-a6fa9ae30645';
|
||||
|
||||
// View fields - Resend email view
|
||||
export const RESEND_EMAIL_VIEW_SUBJECT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'3a3ee801-6dfa-43b4-ba22-5c079a2d238d';
|
||||
export const RESEND_EMAIL_VIEW_FROM_ADDRESS_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'486e23dd-3d8b-42d8-86a8-146d5fd7aaf0';
|
||||
export const RESEND_EMAIL_VIEW_LAST_EVENT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'94e9b3ce-c858-4b15-96ca-69fedf834853';
|
||||
export const RESEND_EMAIL_VIEW_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'61dd7f9f-ce22-4fb4-aeee-f36154211cf8';
|
||||
export const RESEND_EMAIL_VIEW_PERSON_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'5b6cf4a7-f11f-49d4-bc05-8f1d6e0ad72a';
|
||||
export const RESEND_EMAIL_VIEW_CONTACT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'abee6dcd-4754-45b2-ae77-d42bec85afad';
|
||||
export const RESEND_EMAIL_VIEW_BROADCAST_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'8d7df695-5ffb-46b7-9a57-ac979988351f';
|
||||
|
||||
// Navigation menu items
|
||||
export const RESEND_FOLDER_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =
|
||||
'd87574b8-f09b-4963-bfa0-9e4afd433035';
|
||||
export const RESEND_TEMPLATE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =
|
||||
'c56546f1-e4f0-4d03-a480-d152e4b8b427';
|
||||
export const RESEND_SEGMENT_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =
|
||||
'6aa7a107-3c7a-4099-9f8e-b1fa21e6f612';
|
||||
export const RESEND_EMAIL_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =
|
||||
'9d99110c-68ba-41c0-bd95-9b829ab84974';
|
||||
export const RESEND_BROADCAST_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =
|
||||
'cb11a15d-2116-4dd2-9f7d-88ffd2271620';
|
||||
export const RESEND_CONTACT_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =
|
||||
'7cf30c1e-3f3c-4250-9141-a6584dc6697b';
|
||||
|
||||
// Page layouts - Resend template record page
|
||||
export const RESEND_TEMPLATE_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
|
||||
'466cb8b7-70ec-4bb8-802d-6a6bdab4f647';
|
||||
export const RESEND_TEMPLATE_RECORD_PAGE_HOME_TAB_UNIVERSAL_IDENTIFIER =
|
||||
'f8106373-a1a2-4bb8-86b5-0faf8ed52953';
|
||||
export const RESEND_TEMPLATE_RECORD_PAGE_HOME_FIELDS_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
'fa3ad75b-b700-4450-8961-c4395a10ba9f';
|
||||
export const RESEND_TEMPLATE_RECORD_PAGE_PREVIEW_TAB_UNIVERSAL_IDENTIFIER =
|
||||
'56206825-2f86-40be-b311-f5ebc91e016b';
|
||||
export const RESEND_TEMPLATE_RECORD_PAGE_PREVIEW_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
'534b14b0-8ed1-414b-8a83-b631955c2058';
|
||||
export const RESEND_TEMPLATE_RECORD_PAGE_TIMELINE_TAB_UNIVERSAL_IDENTIFIER =
|
||||
'8b62586a-f976-4bae-8ec0-696369e8ec0f';
|
||||
export const RESEND_TEMPLATE_RECORD_PAGE_TIMELINE_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
'4099d844-7104-4fc0-8cb0-b1e0f88d9d33';
|
||||
export const RESEND_TEMPLATE_RECORD_PAGE_TASKS_TAB_UNIVERSAL_IDENTIFIER =
|
||||
'5dc2596a-28c8-4cd1-8500-6648b576f64a';
|
||||
export const RESEND_TEMPLATE_RECORD_PAGE_TASKS_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
'2fe143b4-aa7e-43d3-8a4e-becbe94e96a6';
|
||||
export const RESEND_TEMPLATE_RECORD_PAGE_NOTES_TAB_UNIVERSAL_IDENTIFIER =
|
||||
'd82f101a-93fc-44c6-afd9-26dd7a1ba99a';
|
||||
export const RESEND_TEMPLATE_RECORD_PAGE_NOTES_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
'57c3e4e2-d898-40f3-9273-2dbc07746dba';
|
||||
export const RESEND_TEMPLATE_RECORD_PAGE_FILES_TAB_UNIVERSAL_IDENTIFIER =
|
||||
'f017f93c-c611-46ca-9bef-6c5038cf9609';
|
||||
export const RESEND_TEMPLATE_RECORD_PAGE_FILES_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
'3059a217-5322-4744-9ed0-4757591bba1c';
|
||||
|
||||
// Page layouts - Resend email record page
|
||||
export const RESEND_EMAIL_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
|
||||
'e481afa9-f100-4d88-959d-d4b3518583a2';
|
||||
export const RESEND_EMAIL_RECORD_PAGE_HOME_TAB_UNIVERSAL_IDENTIFIER =
|
||||
'7273a427-5920-42c9-8f50-82bebf01f2bd';
|
||||
export const RESEND_EMAIL_RECORD_PAGE_HOME_FIELDS_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
'4eebad57-eb42-4b69-85a6-2e7c1d365b94';
|
||||
export const RESEND_EMAIL_RECORD_PAGE_PREVIEW_TAB_UNIVERSAL_IDENTIFIER =
|
||||
'50299e13-3652-4059-ae1e-db512869d20b';
|
||||
export const RESEND_EMAIL_RECORD_PAGE_PREVIEW_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
'10bedce3-3e4f-4639-8500-e2035241f364';
|
||||
export const RESEND_EMAIL_RECORD_PAGE_TIMELINE_TAB_UNIVERSAL_IDENTIFIER =
|
||||
'e6c548d4-c371-4b4c-b59c-5b5f4fe50b11';
|
||||
export const RESEND_EMAIL_RECORD_PAGE_TIMELINE_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
'e45c80c7-d5b7-4109-aa5a-ab534d7c4ca2';
|
||||
export const RESEND_EMAIL_RECORD_PAGE_TASKS_TAB_UNIVERSAL_IDENTIFIER =
|
||||
'bce4141e-d115-43ea-94f0-5c45d0ab38b2';
|
||||
export const RESEND_EMAIL_RECORD_PAGE_TASKS_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
'9eab60e5-e305-482d-aec0-ab928a20c855';
|
||||
export const RESEND_EMAIL_RECORD_PAGE_NOTES_TAB_UNIVERSAL_IDENTIFIER =
|
||||
'45ada37c-1f26-4552-9726-308638304ddc';
|
||||
export const RESEND_EMAIL_RECORD_PAGE_NOTES_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
'a4edbea4-a5c3-4316-b6a7-c46fc97d36cd';
|
||||
export const RESEND_EMAIL_RECORD_PAGE_FILES_TAB_UNIVERSAL_IDENTIFIER =
|
||||
'c2f8a3d1-7e49-4b56-9c0a-8d1e5f3b7a92';
|
||||
export const RESEND_EMAIL_RECORD_PAGE_FILES_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
'b9d4e6f2-1a38-4c75-8b0d-3f7a9c2e5d14';
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type HtmlPreviewProps = {
|
||||
html: string | null | undefined;
|
||||
};
|
||||
|
||||
export const HtmlPreview = ({ html }: HtmlPreviewProps) => {
|
||||
if (!isDefined(html) || !isNonEmptyString(html)) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
color: '#999',
|
||||
fontFamily: 'sans-serif',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
No HTML content available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<iframe
|
||||
srcDoc={html}
|
||||
sandbox=""
|
||||
title="Email HTML preview"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { HtmlPreview } from 'src/modules/resend/html-viewer/components/HtmlPreview';
|
||||
import { useRecordHtml } from 'src/modules/resend/html-viewer/hooks/useRecordHtml';
|
||||
|
||||
type RecordHtmlViewerProps = {
|
||||
objectName: string;
|
||||
loadingText: string;
|
||||
};
|
||||
|
||||
export const RecordHtmlViewer = ({
|
||||
objectName,
|
||||
loadingText,
|
||||
}: RecordHtmlViewerProps) => {
|
||||
const { html, loading, error } = useRecordHtml(objectName);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
color: '#999',
|
||||
fontFamily: 'sans-serif',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
{loadingText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(error)) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '16px',
|
||||
color: '#999',
|
||||
fontFamily: 'sans-serif',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
<div>{error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%' }}>
|
||||
<HtmlPreview html={html} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
|
||||
import { RecordHtmlViewer } from 'src/modules/resend/html-viewer/components/RecordHtmlViewer';
|
||||
import { EMAIL_HTML_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/modules/resend/constants/universal-identifiers';
|
||||
|
||||
const EmailHtmlViewer = () => (
|
||||
<RecordHtmlViewer objectName="resendEmail" loadingText="Loading email..." />
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: EMAIL_HTML_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Email HTML Viewer',
|
||||
description: 'Renders the HTML body of a Resend email',
|
||||
component: EmailHtmlViewer,
|
||||
});
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
|
||||
import { RecordHtmlViewer } from 'src/modules/resend/html-viewer/components/RecordHtmlViewer';
|
||||
import { TEMPLATE_HTML_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/modules/resend/constants/universal-identifiers';
|
||||
|
||||
const TemplateHtmlViewer = () => (
|
||||
<RecordHtmlViewer
|
||||
objectName="resendTemplate"
|
||||
loadingText="Loading template..."
|
||||
/>
|
||||
);
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier:
|
||||
TEMPLATE_HTML_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Template HTML Viewer',
|
||||
description: 'Renders the HTML body of a Resend email template',
|
||||
component: TemplateHtmlViewer,
|
||||
});
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecordId } from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type RecordHtmlState = {
|
||||
html: string | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
export const useRecordHtml = (objectName: string): RecordHtmlState => {
|
||||
const recordId = useRecordId();
|
||||
const [state, setState] = useState<RecordHtmlState>({
|
||||
html: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(recordId)) {
|
||||
setState({ html: null, loading: false, error: 'No record ID' });
|
||||
return;
|
||||
}
|
||||
|
||||
setState({ html: null, loading: true, error: null });
|
||||
|
||||
new CoreApiClient()
|
||||
.query({
|
||||
[objectName]: {
|
||||
__args: { filter: { id: { eq: recordId } } },
|
||||
htmlBody: true,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
const record = (result as Record<string, unknown>)[objectName] as
|
||||
| { htmlBody?: string | null }
|
||||
| undefined;
|
||||
|
||||
setState(
|
||||
isDefined(record)
|
||||
? { html: record.htmlBody ?? null, loading: false, error: null }
|
||||
: { html: null, loading: false, error: 'Record not found' },
|
||||
);
|
||||
})
|
||||
.catch((fetchError: unknown) => {
|
||||
setState({
|
||||
html: null,
|
||||
loading: false,
|
||||
error:
|
||||
fetchError instanceof Error
|
||||
? fetchError.message
|
||||
: String(fetchError),
|
||||
});
|
||||
});
|
||||
}, [recordId, objectName]);
|
||||
|
||||
return state;
|
||||
};
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import {
|
||||
Command,
|
||||
defineFrontComponent,
|
||||
enqueueSnackbar,
|
||||
updateProgress,
|
||||
} from 'twenty-sdk';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
SYNC_RESEND_DATA_COMMAND_UNIVERSAL_IDENTIFIER,
|
||||
SYNC_RESEND_DATA_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
SYNC_RESEND_DATA_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
|
||||
const execute = async () => {
|
||||
await updateProgress(0.1);
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
||||
const { findManyLogicFunctions } = await metadataClient.query({
|
||||
findManyLogicFunctions: {
|
||||
id: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const syncFunction = findManyLogicFunctions.find(
|
||||
(fn) =>
|
||||
fn.universalIdentifier ===
|
||||
SYNC_RESEND_DATA_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
if (!isDefined(syncFunction)) {
|
||||
throw new Error('Sync logic function not found');
|
||||
}
|
||||
|
||||
await updateProgress(0.3);
|
||||
|
||||
const { executeOneLogicFunction } = await metadataClient.mutation({
|
||||
executeOneLogicFunction: {
|
||||
__args: {
|
||||
input: {
|
||||
id: syncFunction.id,
|
||||
payload: {} as Record<string, never>,
|
||||
},
|
||||
},
|
||||
status: true,
|
||||
error: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (executeOneLogicFunction.status !== 'SUCCESS') {
|
||||
const rawMessage =
|
||||
typeof executeOneLogicFunction.error?.errorMessage === 'string'
|
||||
? executeOneLogicFunction.error.errorMessage
|
||||
: 'Sync logic function execution failed';
|
||||
|
||||
const isRateLimit =
|
||||
rawMessage.toLowerCase().includes('rate_limit') ||
|
||||
rawMessage.toLowerCase().includes('rate limit');
|
||||
|
||||
throw new Error(
|
||||
isRateLimit
|
||||
? 'Sync failed: Resend API rate limit exceeded. Please try again later.'
|
||||
: `Sync failed: ${rawMessage}`,
|
||||
);
|
||||
}
|
||||
|
||||
await updateProgress(1);
|
||||
|
||||
await enqueueSnackbar({
|
||||
message: 'Resend data sync completed',
|
||||
variant: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
const SyncResendData = () => <Command execute={execute} />;
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: SYNC_RESEND_DATA_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Sync Resend Data',
|
||||
description: 'Triggers a manual sync of all Resend data',
|
||||
isHeadless: true,
|
||||
component: SyncResendData,
|
||||
command: {
|
||||
universalIdentifier: SYNC_RESEND_DATA_COMMAND_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Sync Resend data',
|
||||
icon: 'IconRefresh',
|
||||
isPinned: false,
|
||||
availabilityType: 'GLOBAL',
|
||||
},
|
||||
});
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
import {
|
||||
BROADCAST_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_BROADCAST_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_EMAILS_ON_BROADCAST_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_EMAIL_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: BROADCAST_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: RESEND_EMAIL_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'broadcast',
|
||||
label: 'Broadcast',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
RESEND_BROADCAST_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
RESEND_EMAILS_ON_BROADCAST_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
joinColumnName: 'broadcastId',
|
||||
},
|
||||
icon: 'IconSpeakerphone',
|
||||
});
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
import {
|
||||
CONTACT_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_CONTACT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_EMAILS_ON_CONTACT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_EMAIL_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: CONTACT_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: RESEND_EMAIL_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'contact',
|
||||
label: 'Contact',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
RESEND_CONTACT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
RESEND_EMAILS_ON_CONTACT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
joinColumnName: 'contactId',
|
||||
},
|
||||
icon: 'IconAddressBook',
|
||||
});
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import {
|
||||
PERSON_ON_RESEND_CONTACT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_CONTACTS_ON_PERSON_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_CONTACT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
RelationType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PERSON_ON_RESEND_CONTACT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: RESEND_CONTACT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'person',
|
||||
label: 'Person',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
RESEND_CONTACTS_ON_PERSON_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
joinColumnName: 'personId',
|
||||
},
|
||||
icon: 'IconUser',
|
||||
});
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import {
|
||||
PERSON_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_EMAILS_ON_PERSON_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_EMAIL_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
RelationType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PERSON_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: RESEND_EMAIL_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'person',
|
||||
label: 'Person',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
RESEND_EMAILS_ON_PERSON_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
joinColumnName: 'personId',
|
||||
},
|
||||
icon: 'IconUser',
|
||||
});
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import {
|
||||
RESEND_BROADCASTS_ON_SEGMENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_BROADCAST_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_SEGMENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
SEGMENT_ON_RESEND_BROADCAST_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: RESEND_BROADCASTS_ON_SEGMENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: RESEND_SEGMENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'resendBroadcasts',
|
||||
label: 'Broadcasts',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
RESEND_BROADCAST_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
SEGMENT_ON_RESEND_BROADCAST_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
icon: 'IconSpeakerphone',
|
||||
});
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import {
|
||||
PERSON_ON_RESEND_CONTACT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_CONTACTS_ON_PERSON_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_CONTACT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
RelationType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: RESEND_CONTACTS_ON_PERSON_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
type: FieldType.RELATION,
|
||||
name: 'resendContacts',
|
||||
label: 'Resend Contacts',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
RESEND_CONTACT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
PERSON_ON_RESEND_CONTACT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
icon: 'IconAddressBook',
|
||||
});
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import {
|
||||
RESEND_CONTACTS_ON_SEGMENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_CONTACT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_SEGMENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
SEGMENT_ON_RESEND_CONTACT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: RESEND_CONTACTS_ON_SEGMENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: RESEND_SEGMENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'resendContacts',
|
||||
label: 'Contacts',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
RESEND_CONTACT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
SEGMENT_ON_RESEND_CONTACT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
icon: 'IconAddressBook',
|
||||
});
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import {
|
||||
BROADCAST_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_BROADCAST_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_EMAILS_ON_BROADCAST_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_EMAIL_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: RESEND_EMAILS_ON_BROADCAST_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: RESEND_BROADCAST_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'resendEmails',
|
||||
label: 'Emails',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
RESEND_EMAIL_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
BROADCAST_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
icon: 'IconMail',
|
||||
});
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import {
|
||||
CONTACT_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_CONTACT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_EMAILS_ON_CONTACT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_EMAIL_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: RESEND_EMAILS_ON_CONTACT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: RESEND_CONTACT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'resendEmails',
|
||||
label: 'Emails',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
RESEND_EMAIL_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
CONTACT_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
icon: 'IconMail',
|
||||
});
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import {
|
||||
PERSON_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_EMAILS_ON_PERSON_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_EMAIL_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
RelationType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: RESEND_EMAILS_ON_PERSON_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
type: FieldType.RELATION,
|
||||
name: 'resendEmails',
|
||||
label: 'Resend Emails',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
RESEND_EMAIL_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
PERSON_ON_RESEND_EMAIL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
icon: 'IconMail',
|
||||
});
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
import {
|
||||
RESEND_BROADCASTS_ON_SEGMENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_BROADCAST_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_SEGMENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
SEGMENT_ON_RESEND_BROADCAST_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: SEGMENT_ON_RESEND_BROADCAST_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: RESEND_BROADCAST_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'segment',
|
||||
label: 'Segment',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
RESEND_SEGMENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
RESEND_BROADCASTS_ON_SEGMENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
joinColumnName: 'segmentId',
|
||||
},
|
||||
icon: 'IconUsersGroup',
|
||||
});
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
import {
|
||||
RESEND_CONTACTS_ON_SEGMENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_CONTACT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_SEGMENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
SEGMENT_ON_RESEND_CONTACT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
import { defineField, FieldType, RelationType } from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: SEGMENT_ON_RESEND_CONTACT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: RESEND_CONTACT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'segment',
|
||||
label: 'Segment',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
RESEND_SEGMENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
RESEND_CONTACTS_ON_SEGMENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
joinColumnName: 'segmentId',
|
||||
},
|
||||
icon: 'IconUsersGroup',
|
||||
});
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import {
|
||||
RESEND_BROADCAST_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_BROADCAST_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_FOLDER_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier:
|
||||
RESEND_BROADCAST_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Broadcasts',
|
||||
icon: 'IconSpeakerphone',
|
||||
position: 2,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: RESEND_BROADCAST_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
folderUniversalIdentifier:
|
||||
RESEND_FOLDER_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import {
|
||||
RESEND_CONTACT_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_CONTACT_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
RESEND_FOLDER_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/resend/constants/universal-identifiers';
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: RESEND_CONTACT_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Contacts',
|
||||
icon: 'IconAddressBook',
|
||||
position: 1,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: RESEND_CONTACT_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
folderUniversalIdentifier:
|
||||
RESEND_FOLDER_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user