snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# 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

View File

@@ -0,0 +1 @@
24.5.0

View File

@@ -0,0 +1,58 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"extends": ["../../.oxlintrc.base.json"],
"plugins": ["react", "typescript", "import", "unicorn"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": "off",
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-redeclare": "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": "^_"
}
],
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"react/jsx-key": "off",
"react/display-name": "off",
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"react/jsx-no-useless-fragment": "off",
"react/jsx-props-no-spreading": ["error", { "explicitSpread": "ignore" }],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}

View File

@@ -0,0 +1 @@
nodeLinker: node-modules

View File

@@ -0,0 +1,13 @@
## 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 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.

View File

@@ -0,0 +1,13 @@
## 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 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.

View File

@@ -0,0 +1,13 @@
## 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 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.

View File

@@ -0,0 +1,55 @@
# Linear for Twenty
Connect your Linear account to Twenty to create issues and look up teams
straight from your workflows or the AI chat.
## What you can do
Once installed and connected, two tools become available:
- **Create Linear issue** — from the AI chat, ask something like
*"create a Linear issue in the Engineering team titled 'Fix login bug'"*
and the AI will file it for you. From a workflow, add it as a step
with `teamId` + `title` (and optional `description`).
- **List Linear teams** — discovers the teams in your Linear workspace,
useful when you need to pick a `teamId` for the create-issue step.
## Installing
1. Open **Settings → Applications** in your Twenty workspace.
2. Find **Linear** in the available apps and click **Install**.
3. Open the app, go to the **Connections** tab, and click **Add connection**.
4. Choose **Just for me** (your personal Linear account) or
**Workspace shared** (a team-managed Linear account anyone in this
workspace can act through), then complete the Linear sign-in.
That's it — you can now use the tools above.
> If you see a "Linear OAuth is not yet set up by your server administrator"
> notice on the Connections tab, ask your Twenty admin to follow the
> **Self-hosting setup** below — they need to provide the OAuth credentials
> before connections can be added.
---
## Self-hosting setup
This section is for Twenty server admins. If you're on Twenty Cloud, skip
this — the OAuth credentials are already configured.
### 1. Register an OAuth app in Linear
1. Visit https://linear.app/settings/api/applications/new.
2. Set the **Redirect URI** to `<SERVER_URL>/auth/apps/callback` (for
local dev: `http://localhost:3000/auth/apps/callback`).
3. Copy the generated **Client ID** and **Client Secret**.
### 2. Wire the credentials into Twenty
1. In **Settings → Applications**, find **Linear**, click into it, and go
to the **Application registration** tab (admin-only).
2. Paste your Linear **Client ID** into `LINEAR_CLIENT_ID` and the
**Client Secret** into `LINEAR_CLIENT_SECRET`.
Workspace users will now be able to add Linear connections from the
**Connections** tab as described above.

View File

@@ -0,0 +1,38 @@
{
"name": "@twentyhq/linear",
"version": "0.2.0",
"description": "Linear integration for Twenty. Connect a user's Linear account and create issues from logic functions.",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2",
"twenty": ">=2.37.0"
},
"keywords": [
"twenty-app"
],
"packageManager": "yarn@4.13.0",
"scripts": {
"twenty": "twenty",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
"typecheck": "tsgo --noEmit -p tsconfig.spec.json",
"test": "vitest run",
"test:watch": "vitest",
"test:unit": "vitest run --config vitest.unit.config.ts"
},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/react": "^18.2.0",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"oxlint": "^0.16.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"twenty-client-sdk": "2.37.0",
"twenty-sdk": "2.37.0",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^4.0.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

View File

@@ -0,0 +1 @@
<svg fill="#5E6AD2" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Linear</title><path d="M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z"/></svg>

After

Width:  |  Height:  |  Size: 469 B

View File

@@ -0,0 +1,87 @@
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 docker: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'}`,
);
}
}

View File

@@ -0,0 +1,25 @@
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { describe, expect, it } from 'vitest';
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
describe('App installation', () => {
it('should find the installed Linear app in the applications list', async () => {
const client = new MetadataApiClient();
const result = await client.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const matchingApplication = result.findManyApplications.find(
(application: { universalIdentifier: string }) =>
application.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(matchingApplication).toBeDefined();
});
});

View File

@@ -0,0 +1,36 @@
import { defineApplication } from 'twenty-sdk/define';
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'Linear',
description:
'Connect Linear to Twenty. Each workspace member connects their own Linear account; logic functions can then create issues and read team data on their behalf.',
logoUrl: 'public/linear-logomark.svg',
applicationVariables: undefined,
author: 'Twenty',
category: 'Product management',
emailSupport: 'contact@twenty.com',
screenshots: [
'public/gallery/command-menu-item-1.png',
'public/gallery/command-menu-item-2.png',
'public/gallery/command-menu-item-3.png',
'public/gallery/command-menu-item-4.png',
],
termsUrl: 'https://github.com/twentyhq/twenty?tab=License-1-ov-file#readme',
websiteUrl: 'https://www.twenty.com',
serverVariables: {
LINEAR_CLIENT_ID: {
description:
'OAuth client ID from your Linear OAuth application (linear.app/settings/api/applications).',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description:
'OAuth client secret from your Linear OAuth application. Stored encrypted; never exposed in API responses.',
isSecret: true,
isRequired: true,
},
},
});

View File

@@ -0,0 +1,17 @@
import { defineCommandMenuItem } from 'twenty-sdk/define';
import {
CREATE_ISSUE_COMMAND_UNIVERSAL_IDENTIFIER,
CREATE_ISSUE_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineCommandMenuItem({
universalIdentifier: CREATE_ISSUE_COMMAND_UNIVERSAL_IDENTIFIER,
label: 'Create Linear issue',
shortLabel: 'Linear issue',
icon: 'IconPlaylistAdd',
isPinned: false,
availabilityType: 'GLOBAL',
frontComponentUniversalIdentifier:
CREATE_ISSUE_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
});

View File

@@ -0,0 +1,20 @@
import { defineConnectionProvider } from 'twenty-sdk/define';
import { LINEAR_CONNECTION_PROVIDER_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
export default defineConnectionProvider({
universalIdentifier: LINEAR_CONNECTION_PROVIDER_UNIVERSAL_IDENTIFIER,
name: 'linear',
displayName: 'Linear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
revokeEndpoint: 'https://api.linear.app/oauth/revoke',
scopes: ['read', 'write'],
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
tokenRequestContentType: 'form-urlencoded',
usePkce: true,
},
});

View File

@@ -0,0 +1,40 @@
// Group all universal identifiers in a single file. Per the codebase
// convention (see twenty-for-twenty), closely-related constants live
// together so the rest of the app's source files can stay at one
// `export default` per file.
export const APPLICATION_UNIVERSAL_IDENTIFIER =
'6f4e7c2a-3d8e-4a91-b2cf-9e0b8d5f4a2e';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b6c33347-e41c-4b90-8a37-7b3c49baa85a';
export const LINEAR_CONNECTION_PROVIDER_UNIVERSAL_IDENTIFIER =
'9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f';
export const CREATE_LINEAR_ISSUE_UNIVERSAL_IDENTIFIER =
'01f829c9-1661-41fa-9ee1-b67e64716c2e';
export const LIST_LINEAR_TEAMS_UNIVERSAL_IDENTIFIER =
'15824bbc-9c64-4f97-b45f-d0a44b402bb8';
export const LIST_LINEAR_TEAMS_ROUTE_UNIVERSAL_IDENTIFIER =
'a1d4e7b3-5c2f-4a89-9b0e-6f3d8c1a7e5b';
export const CREATE_LINEAR_ISSUE_ROUTE_UNIVERSAL_IDENTIFIER =
'c8f2a6d1-3b7e-4e09-8d5c-2a9f0b4e6c3d';
export const CREATE_ISSUE_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
'd4a1c8e5-7f3b-4d62-a90e-1b5c3f8d2a6e';
export const LIST_LINEAR_WORKFLOW_STATES_ROUTE_UNIVERSAL_IDENTIFIER =
'f5a2d8c1-4b7e-4f93-a6d0-9c3e1b8f5a2d';
export const LIST_LINEAR_ISSUE_OPTIONS_ROUTE_UNIVERSAL_IDENTIFIER =
'a3b7e1d9-8c4f-4a26-b5d0-7e9f2c6a8b3d';
export const LIST_LINEAR_ISSUE_OPTIONS_UNIVERSAL_IDENTIFIER =
'c2b99086-6c1b-43e4-9685-de4ade6bda63';
export const CREATE_ISSUE_COMMAND_UNIVERSAL_IDENTIFIER =
'e7b3d9f2-6a1c-4e85-b4d0-8f2c5a7e3b19';

View File

@@ -0,0 +1,126 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createLinearIssueHandler } from '../handlers/create-linear-issue-handler';
import { buildConnection, stubConnectionsThenLinear } from './test-utils';
const SAVED_ENV = { ...process.env };
describe('createLinearIssueHandler', () => {
beforeEach(() => {
process.env.TWENTY_API_URL = 'http://api.test';
process.env.TWENTY_APP_ACCESS_TOKEN = 'app-token';
});
afterEach(() => {
process.env = { ...SAVED_ENV };
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('returns an error when required input fields are missing', async () => {
const result = await createLinearIssueHandler({ title: 'no team' });
expect(result).toMatchObject({
success: false,
error: expect.stringContaining('teamId'),
});
});
it('returns an error when no Linear connection exists', async () => {
stubConnectionsThenLinear([], {});
const result = await createLinearIssueHandler({
teamId: 'team_1',
title: 'hi',
});
expect(result).toMatchObject({
success: false,
error: expect.stringContaining('not connected'),
});
});
it('calls Linear with the only available connection and returns the issue', async () => {
const issue = {
id: 'issue_1',
identifier: 'TEAM-1',
title: 'Hello from Twenty',
url: 'https://linear.app/twenty/issue/TEAM-1',
};
const fetchMock = stubConnectionsThenLinear([buildConnection()], {
data: { issueCreate: { success: true, issue } },
});
const result = await createLinearIssueHandler({
teamId: 'team_1',
title: 'Hello from Twenty',
description: 'Body',
});
expect(result).toEqual({ success: true, issue });
const [url, init] = fetchMock.mock.calls[1];
expect(url).toBe('https://api.linear.app/graphql');
expect(init.headers.Authorization).toBe('Bearer lin_test_access_token');
expect(JSON.parse(init.body as string).variables.input).toEqual({
teamId: 'team_1',
title: 'Hello from Twenty',
description: 'Body',
});
});
it('prefers a workspace-shared connection over a user-visibility one', async () => {
const userConnection = buildConnection({
id: 'conn_user',
accessToken: 'lin_user',
});
const sharedConnection = buildConnection({
id: 'conn_shared',
visibility: 'workspace',
accessToken: 'lin_shared',
});
const fetchMock = stubConnectionsThenLinear(
[userConnection, sharedConnection],
{
data: {
issueCreate: {
success: true,
issue: {
id: 'issue_2',
identifier: 'T-2',
title: 'Hi',
url: 'https://linear.app/x/T-2',
},
},
},
},
);
const result = await createLinearIssueHandler({
teamId: 'team_1',
title: 'Hi',
});
expect(result.success).toBe(true);
expect(fetchMock.mock.calls[1][1].headers.Authorization).toBe(
'Bearer lin_shared',
);
});
it('surfaces Linear GraphQL errors as the handler error', async () => {
stubConnectionsThenLinear([buildConnection()], {
errors: [{ message: 'Invalid teamId' }],
});
const result = await createLinearIssueHandler({
teamId: 'bogus',
title: 'hi',
});
expect(result).toEqual({ success: false, error: 'Invalid teamId' });
});
});

View File

@@ -0,0 +1,56 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { listLinearTeamsHandler } from '../handlers/list-linear-teams-handler';
import { buildConnection, stubConnectionsThenLinear } from './test-utils';
const SAVED_ENV = { ...process.env };
describe('listLinearTeamsHandler', () => {
beforeEach(() => {
process.env.TWENTY_API_URL = 'http://api.test';
process.env.TWENTY_APP_ACCESS_TOKEN = 'app-token';
});
afterEach(() => {
process.env = { ...SAVED_ENV };
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('returns the teams when the Linear query succeeds', async () => {
const teams = [
{ id: 'team_1', name: 'Engineering', key: 'ENG' },
{ id: 'team_2', name: 'Design', key: 'DES' },
];
const fetchMock = stubConnectionsThenLinear([buildConnection()], {
data: { teams: { nodes: teams } },
});
const result = await listLinearTeamsHandler();
expect(result).toEqual({ success: true, teams });
expect(fetchMock.mock.calls[1][1].headers.Authorization).toBe(
'Bearer lin_test_access_token',
);
});
it('returns success=false when no Linear connection exists', async () => {
stubConnectionsThenLinear([], { data: { teams: { nodes: [] } } });
const result = await listLinearTeamsHandler();
expect(result.success).toBe(false);
});
it('surfaces Linear errors', async () => {
stubConnectionsThenLinear([buildConnection()], {
errors: [{ message: 'rate limited' }],
});
const result = await listLinearTeamsHandler();
expect(result).toEqual({ success: false, error: 'rate limited' });
});
});

View File

@@ -0,0 +1,56 @@
import { vi } from 'vitest';
export const USER_WORKSPACE_ID = '11111111-1111-1111-1111-111111111111';
type LinearFetchInit = {
method: string;
headers: Record<string, string>;
body: string;
};
export const buildConnection = (
overrides: Partial<Record<string, unknown>> = {},
) => ({
id: 'conn_1',
name: 'octocat@example.com',
visibility: 'user' as const,
providerName: 'linear',
userWorkspaceId: USER_WORKSPACE_ID,
accessToken: 'lin_test_access_token',
scopes: ['read', 'write'],
handle: 'octocat@example.com',
lastRefreshedAt: '2024-01-01T00:00:00.000Z',
authFailedAt: null,
...overrides,
});
// Stubs `fetch` to first answer the SDK's `listConnections` GraphQL call to
// `/metadata`, then the handler's downstream Linear GraphQL request.
export const stubConnectionsThenLinear = (
connections: ReturnType<typeof buildConnection>[],
linearJson: unknown,
) => {
const fetchMock = vi.fn(async (url: string, _init: LinearFetchInit) => {
if (url.endsWith('/metadata')) {
const connectionsJson = { data: { appConnections: connections } };
return {
ok: true,
status: 200,
json: async () => connectionsJson,
text: async () => JSON.stringify(connectionsJson),
};
}
return {
ok: true,
status: 200,
json: async () => linearJson,
text: async () => JSON.stringify(linearJson),
};
});
vi.stubGlobal('fetch', fetchMock);
return fetchMock;
};

View File

@@ -0,0 +1,8 @@
export const ISSUE_CREATE_MUTATION = `
mutation IssueCreate($input: IssueCreateInput!) {
issueCreate(input: $input) {
success
issue { id identifier title url }
}
}
`;

View File

@@ -0,0 +1,34 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { CREATE_LINEAR_ISSUE_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { createLinearIssueHandler } from 'src/logic-functions/handlers/create-linear-issue-handler';
const handler = async (event: RoutePayload) => {
const body = event.body as Record<string, unknown> | null;
return createLinearIssueHandler({
teamId: body?.teamId as string | undefined,
title: body?.title as string | undefined,
description: body?.description as string | undefined,
priority: body?.priority as number | undefined,
stateId: body?.stateId as string | undefined,
assigneeId: body?.assigneeId as string | undefined,
projectId: body?.projectId as string | undefined,
estimate: body?.estimate as number | undefined,
labelIds: body?.labelIds as string[] | undefined,
cycleId: body?.cycleId as string | undefined,
dueDate: body?.dueDate as string | undefined,
});
};
export default defineLogicFunction({
universalIdentifier: CREATE_LINEAR_ISSUE_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'create-linear-issue-route',
timeoutSeconds: 30,
handler,
httpRouteTriggerSettings: {
path: '/linear/issues',
httpMethod: 'POST',
isAuthRequired: true,
},
});

View File

@@ -0,0 +1,112 @@
import { defineLogicFunction } from 'twenty-sdk/define';
import { CREATE_LINEAR_ISSUE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { createLinearIssueHandler } from 'src/logic-functions/handlers/create-linear-issue-handler';
export default defineLogicFunction({
universalIdentifier: CREATE_LINEAR_ISSUE_UNIVERSAL_IDENTIFIER,
name: 'create-linear-issue',
description:
'Create a Linear issue on behalf of the connected user. Requires a teamId (call list-linear-teams to discover one) and a title.',
timeoutSeconds: 30,
handler: createLinearIssueHandler,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
teamId: {
type: 'string',
description:
'The Linear team ID to create the issue in. Use list-linear-teams to discover available teams.',
},
title: {
type: 'string',
description: 'The issue title.',
},
description: {
type: 'string',
description: 'Optional issue description (Markdown supported).',
},
priority: {
type: 'integer',
description:
'Issue priority: 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low.',
minimum: 0,
maximum: 4,
},
stateId: {
type: 'string',
description:
'The workflow state ID for the issue status. Use list-linear-issue-options to discover available states for a team.',
},
assigneeId: {
type: 'string',
description:
'The user ID to assign the issue to. Use list-linear-issue-options to discover team members.',
},
projectId: {
type: 'string',
description: 'The project ID to associate the issue with.',
},
estimate: {
type: 'number',
description:
'The estimate value for the issue. Must match the team estimate scale.',
},
labelIds: {
type: 'array',
items: { type: 'string' },
description: 'Array of label IDs to apply to the issue.',
},
cycleId: {
type: 'string',
description: 'The cycle ID to add the issue to.',
},
dueDate: {
type: 'string',
description: 'Due date in YYYY-MM-DD format.',
},
},
required: ['teamId', 'title'],
},
},
workflowActionTriggerSettings: {
label: 'Create Linear Issue',
inputSchema: [
{
type: 'object',
properties: {
teamId: { type: 'string' },
title: { type: 'string' },
description: { type: 'string' },
priority: { type: 'number' },
stateId: { type: 'string' },
assigneeId: { type: 'string' },
projectId: { type: 'string' },
estimate: { type: 'number' },
labelIds: { type: 'array', items: { type: 'string' } },
cycleId: { type: 'string' },
dueDate: { type: 'string' },
},
},
],
outputSchema: [
{
type: 'object',
properties: {
success: { type: 'boolean' },
issue: {
type: 'object',
properties: {
id: { type: 'string' },
identifier: { type: 'string' },
title: { type: 'string' },
url: { type: 'string' },
},
},
error: { type: 'string' },
},
},
],
},
});

View File

@@ -0,0 +1,84 @@
import { listConnections } from 'twenty-sdk/logic-function';
import { ISSUE_CREATE_MUTATION } from 'src/logic-functions/constants/issue-create-mutation.constant';
import { type CreateIssueInput } from 'src/logic-functions/types/create-issue-input.type';
import { type CreateIssueMutationResult } from 'src/logic-functions/types/create-issue-mutation-result.type';
import { callLinearGraphQL } from 'src/logic-functions/utils/call-linear-graphql';
type HandlerResult =
| {
success: true;
issue: {
id: string;
identifier: string;
title: string;
url: string;
};
}
| { success: false; error: string };
export const createLinearIssueHandler = async (
input: CreateIssueInput,
): Promise<HandlerResult> => {
if (!input.teamId || !input.title) {
return {
success: false,
error: 'Both `teamId` and `title` are required.',
};
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present (a team-managed service
// account); otherwise fall back to the first user-scoped connection.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection" first.',
};
}
const result = await callLinearGraphQL<CreateIssueMutationResult>({
accessToken: connection.accessToken,
query: ISSUE_CREATE_MUTATION,
variables: {
input: {
teamId: input.teamId,
title: input.title,
description: input.description,
...(input.priority !== undefined && { priority: input.priority }),
...(input.stateId !== undefined && { stateId: input.stateId }),
...(input.assigneeId !== undefined && {
assigneeId: input.assigneeId,
}),
...(input.projectId !== undefined && { projectId: input.projectId }),
...(input.estimate !== undefined && { estimate: input.estimate }),
...(input.labelIds !== undefined &&
input.labelIds.length > 0 && { labelIds: input.labelIds }),
...(input.cycleId !== undefined && { cycleId: input.cycleId }),
...(input.dueDate !== undefined && { dueDate: input.dueDate }),
},
},
});
if (result.errors || !result.data) {
return {
success: false,
error: result.errors?.[0]?.message ?? 'Unknown Linear API error',
};
}
const { success, issue } = result.data.issueCreate;
if (!success || !issue) {
return {
success: false,
error: 'Linear reported the mutation as unsuccessful.',
};
}
return { success: true, issue };
};

View File

@@ -0,0 +1,129 @@
import { listConnections } from 'twenty-sdk/logic-function';
import { callLinearGraphQL } from 'src/logic-functions/utils/call-linear-graphql';
type LinearMember = {
id: string;
name: string;
displayName: string;
};
type LinearProject = {
id: string;
name: string;
};
type LinearLabel = {
id: string;
name: string;
color: string;
};
type LinearCycle = {
id: string;
name: string | null;
number: number;
startsAt: string;
endsAt: string;
};
type LinearWorkflowState = {
id: string;
name: string;
type: string;
position: number;
};
type IssueOptionsQueryResult = {
team: {
states: { nodes: LinearWorkflowState[] };
members: { nodes: LinearMember[] };
cycles: { nodes: LinearCycle[] };
issueEstimationType: string;
issueEstimationAllowZero: boolean;
};
projects: { nodes: LinearProject[] };
issueLabels: { nodes: LinearLabel[] };
};
type IssueOptions = {
states: LinearWorkflowState[];
members: LinearMember[];
projects: LinearProject[];
labels: LinearLabel[];
cycles: LinearCycle[];
estimationType: string;
estimationAllowZero: boolean;
};
type HandlerResult =
| { success: true; options: IssueOptions }
| { success: false; error: string };
export const listLinearIssueOptionsHandler = async (input: {
teamId?: string;
}): Promise<HandlerResult> => {
if (!input.teamId) {
return { success: false, error: '`teamId` is required.' };
}
const connections = await listConnections({ providerName: 'linear' });
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection" first.',
};
}
const result = await callLinearGraphQL<IssueOptionsQueryResult>({
accessToken: connection.accessToken,
query: `
query IssueOptions($teamId: String!) {
team(id: $teamId) {
states { nodes { id name type position } }
members { nodes { id name displayName } }
cycles { nodes { id name number startsAt endsAt } }
issueEstimationType
issueEstimationAllowZero
}
projects { nodes { id name } }
issueLabels { nodes { id name color } }
}
`,
variables: { teamId: input.teamId },
});
if (result.errors || !result.data) {
return {
success: false,
error: result.errors?.[0]?.message ?? 'Unknown Linear API error',
};
}
const { team, projects, issueLabels } = result.data;
const now = new Date().toISOString();
const activeCycles = team.cycles.nodes.filter((c) => c.endsAt >= now);
return {
success: true,
options: {
states: team.states.nodes.sort((a, b) => a.position - b.position),
members: team.members.nodes.sort((a, b) =>
a.displayName.localeCompare(b.displayName),
),
projects: projects.nodes.sort((a, b) => a.name.localeCompare(b.name)),
labels: issueLabels.nodes.sort((a, b) => a.name.localeCompare(b.name)),
cycles: activeCycles.sort(
(a, b) =>
new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime(),
),
estimationType: team.issueEstimationType,
estimationAllowZero: team.issueEstimationAllowZero,
},
};
};

View File

@@ -0,0 +1,49 @@
import { listConnections } from 'twenty-sdk/logic-function';
import { callLinearGraphQL } from 'src/logic-functions/utils/call-linear-graphql';
type LinearTeam = {
id: string;
name: string;
key: string;
};
type TeamsQueryResult = {
teams: { nodes: LinearTeam[] };
};
type HandlerResult =
| { success: true; teams: LinearTeam[] }
| { success: false; error: string };
export const listLinearTeamsHandler = async (): Promise<HandlerResult> => {
const connections = await listConnections({ providerName: 'linear' });
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection" first.',
};
}
const result = await callLinearGraphQL<TeamsQueryResult>({
accessToken: connection.accessToken,
query: `
query Teams {
teams { nodes { id name key } }
}
`,
});
if (result.errors || !result.data) {
return {
success: false,
error: result.errors?.[0]?.message ?? 'Unknown Linear API error',
};
}
return { success: true, teams: result.data.teams.nodes };
};

View File

@@ -0,0 +1,63 @@
import { listConnections } from 'twenty-sdk/logic-function';
import { callLinearGraphQL } from 'src/logic-functions/utils/call-linear-graphql';
type LinearWorkflowState = {
id: string;
name: string;
type: string;
position: number;
};
type WorkflowStatesQueryResult = {
team: { states: { nodes: LinearWorkflowState[] } };
};
type HandlerResult =
| { success: true; states: LinearWorkflowState[] }
| { success: false; error: string };
export const listLinearWorkflowStatesHandler = async (input: {
teamId?: string;
}): Promise<HandlerResult> => {
if (!input.teamId) {
return { success: false, error: '`teamId` is required.' };
}
const connections = await listConnections({ providerName: 'linear' });
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection" first.',
};
}
const result = await callLinearGraphQL<WorkflowStatesQueryResult>({
accessToken: connection.accessToken,
query: `
query WorkflowStates($teamId: String!) {
team(id: $teamId) {
states { nodes { id name type position } }
}
}
`,
variables: { teamId: input.teamId },
});
if (result.errors || !result.data) {
return {
success: false,
error: result.errors?.[0]?.message ?? 'Unknown Linear API error',
};
}
const states = result.data.team.states.nodes.sort(
(a, b) => a.position - b.position,
);
return { success: true, states };
};

View File

@@ -0,0 +1,22 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { LIST_LINEAR_ISSUE_OPTIONS_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { listLinearIssueOptionsHandler } from 'src/logic-functions/handlers/list-linear-issue-options-handler';
const handler = async (event: RoutePayload) => {
return listLinearIssueOptionsHandler({
teamId: event.queryStringParameters?.teamId,
});
};
export default defineLogicFunction({
universalIdentifier: LIST_LINEAR_ISSUE_OPTIONS_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'list-linear-issue-options-route',
timeoutSeconds: 15,
handler,
httpRouteTriggerSettings: {
path: '/linear/issue-options',
httpMethod: 'GET',
isAuthRequired: true,
},
});

View File

@@ -0,0 +1,110 @@
import { defineLogicFunction } from 'twenty-sdk/define';
import { LIST_LINEAR_ISSUE_OPTIONS_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { listLinearIssueOptionsHandler } from 'src/logic-functions/handlers/list-linear-issue-options-handler';
export default defineLogicFunction({
universalIdentifier: LIST_LINEAR_ISSUE_OPTIONS_UNIVERSAL_IDENTIFIER,
name: 'list-linear-issue-options',
description:
'Returns available options for creating a Linear issue in a specific team: workflow states, members, projects, labels, cycles, and estimation settings. Requires a teamId (call list-linear-teams to discover one).',
timeoutSeconds: 15,
handler: listLinearIssueOptionsHandler,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {
teamId: {
type: 'string',
description:
'The Linear team ID to fetch issue options for. Use list-linear-teams to discover available teams.',
},
},
required: ['teamId'],
},
},
workflowActionTriggerSettings: {
label: 'List Linear Issue Options',
inputSchema: [
{
type: 'object',
properties: {
teamId: { type: 'string' },
},
},
],
outputSchema: [
{
type: 'object',
properties: {
success: { type: 'boolean' },
options: {
type: 'object',
properties: {
states: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
type: { type: 'string' },
position: { type: 'number' },
},
},
},
members: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
displayName: { type: 'string' },
},
},
},
projects: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
},
},
},
labels: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
color: { type: 'string' },
},
},
},
cycles: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
number: { type: 'number' },
startsAt: { type: 'string' },
endsAt: { type: 'string' },
},
},
},
estimationType: { type: 'string' },
estimationAllowZero: { type: 'boolean' },
},
},
error: { type: 'string' },
},
},
],
},
});

View File

@@ -0,0 +1,20 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { LIST_LINEAR_TEAMS_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { listLinearTeamsHandler } from 'src/logic-functions/handlers/list-linear-teams-handler';
const handler = async (_event: RoutePayload) => {
return listLinearTeamsHandler();
};
export default defineLogicFunction({
universalIdentifier: LIST_LINEAR_TEAMS_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'list-linear-teams-route',
timeoutSeconds: 15,
handler,
httpRouteTriggerSettings: {
path: '/linear/teams',
httpMethod: 'GET',
isAuthRequired: true,
},
});

View File

@@ -0,0 +1,48 @@
import { defineLogicFunction } from 'twenty-sdk/define';
import { LIST_LINEAR_TEAMS_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { listLinearTeamsHandler } from 'src/logic-functions/handlers/list-linear-teams-handler';
export default defineLogicFunction({
universalIdentifier: LIST_LINEAR_TEAMS_UNIVERSAL_IDENTIFIER,
name: 'list-linear-teams',
description:
"Returns the connected user's Linear teams. Useful for picking a teamId to pass to create-linear-issue.",
timeoutSeconds: 15,
handler: listLinearTeamsHandler,
toolTriggerSettings: {
inputSchema: {
type: 'object',
properties: {},
},
},
workflowActionTriggerSettings: {
label: 'List Linear Teams',
inputSchema: [
{
type: 'object',
properties: {},
},
],
outputSchema: [
{
type: 'object',
properties: {
success: { type: 'boolean' },
teams: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
key: { type: 'string' },
},
},
},
error: { type: 'string' },
},
},
],
},
});

View File

@@ -0,0 +1,22 @@
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
import { LIST_LINEAR_WORKFLOW_STATES_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { listLinearWorkflowStatesHandler } from 'src/logic-functions/handlers/list-linear-workflow-states-handler';
const handler = async (event: RoutePayload) => {
return listLinearWorkflowStatesHandler({
teamId: event.queryStringParameters?.teamId,
});
};
export default defineLogicFunction({
universalIdentifier: LIST_LINEAR_WORKFLOW_STATES_ROUTE_UNIVERSAL_IDENTIFIER,
name: 'list-linear-workflow-states-route',
timeoutSeconds: 15,
handler,
httpRouteTriggerSettings: {
path: '/linear/workflow-states',
httpMethod: 'GET',
isAuthRequired: true,
},
});

View File

@@ -0,0 +1,13 @@
export type CreateIssueInput = {
teamId?: string;
title?: string;
description?: string;
priority?: number;
stateId?: string;
assigneeId?: string;
projectId?: string;
estimate?: number;
labelIds?: string[];
cycleId?: string;
dueDate?: string;
};

View File

@@ -0,0 +1,11 @@
export type CreateIssueMutationResult = {
issueCreate: {
success: boolean;
issue: {
id: string;
identifier: string;
title: string;
url: string;
} | null;
};
};

View File

@@ -0,0 +1,58 @@
import { type LinearGraphQLResult } from 'src/logic-functions/utils/types/linear-graphql-result.type';
const LINEAR_GRAPHQL_ENDPOINT = 'https://api.linear.app/graphql';
export const callLinearGraphQL = async <TData>({
accessToken,
query,
variables,
}: {
accessToken: string;
query: string;
variables?: Record<string, unknown>;
}): Promise<LinearGraphQLResult<TData>> => {
let response: Response;
try {
response = await fetch(LINEAR_GRAPHQL_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ query, variables }),
});
} catch (error) {
return {
errors: [
{
message: `Linear API request failed: ${(error as Error).message}`,
},
],
};
}
if (!response.ok) {
const text = await response.text().catch(() => '');
return {
errors: [
{
message: `Linear API responded with ${response.status}: ${text.slice(0, 500)}`,
},
],
};
}
try {
return (await response.json()) as LinearGraphQLResult<TData>;
} catch (error) {
return {
errors: [
{
message: `Linear API returned a non-JSON response: ${(error as Error).message}`,
},
],
};
}
};

View File

@@ -0,0 +1,4 @@
export type LinearGraphQLResult<TData> = {
data?: TData;
errors?: Array<{ message: string }>;
};

View File

@@ -0,0 +1,20 @@
import { defineApplicationRole } from 'twenty-sdk/define';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Linear function role',
description: 'No-op role for Linear logic functions',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [],
fieldPermissions: [],
permissionFlagUniversalIdentifiers: [],
});

View File

@@ -0,0 +1,42 @@
{
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
"declaration": true,
"outDir": "./dist",
"rootDir": ".",
"jsx": "react-jsx",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"allowUnreachableCode": false,
"strict": true,
"alwaysStrict": true,
"noImplicitAny": true,
"strictBindCallApply": false,
"target": "es2020",
"module": "esnext",
"lib": ["es2020", "dom"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"resolveJsonModule": true,
"paths": {
"src/*": ["./src/*"],
"~/*": ["./*"]
}
},
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts",
"**/*.integration-test.ts"
],
"references": [
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"composite": true,
"types": ["vitest/globals", "node"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,31 @@
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({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
env: {
TWENTY_API_URL,
TWENTY_API_KEY,
},
},
});

View File

@@ -0,0 +1,14 @@
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
include: ['src/**/*.test.ts'],
},
});

File diff suppressed because it is too large Load Diff