mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Refactor twenty client sdk provisioning for logic function and front-component (#18544)
## 1. The `twenty-client-sdk` Package (Source of Truth)
The monorepo package at `packages/twenty-client-sdk` ships with:
- A **pre-built metadata client** (static, generated from a fixed
schema)
- A **stub core client** that throws at runtime (`CoreApiClient was not
generated...`)
- Both ESM (`.mjs`) and CJS (`.cjs`) bundles in `dist/`
- A `package.json` with proper `exports` map for
`twenty-client-sdk/core`, `twenty-client-sdk/metadata`, and
`twenty-client-sdk/generate`
## 2. Generation & Upload (Server-Side, at Migration Time)
**When**: `WorkspaceMigrationRunnerService.run()` executes after a
metadata schema change.
**What happens in `SdkClientGenerationService.generateAndStore()`**:
1. Copies the stub `twenty-client-sdk` package from the server's assets
(resolved via `SDK_CLIENT_PACKAGE_DIRNAME` — from
`dist/assets/twenty-client-sdk/` in production, or from `node_modules`
in dev)
2. Filters out `node_modules/` and `src/` during copy — only
`package.json` + `dist/` are kept (like an npm publish)
3. Calls `replaceCoreClient()` which uses `@genql/cli` to introspect the
**application-scoped** GraphQL schema and generates a real
`CoreApiClient`, then compiles it to ESM+CJS and overwrites
`dist/core.mjs` and `dist/core.cjs`
4. Archives the **entire package** (with `package.json` + `dist/`) into
`twenty-client-sdk.zip`
5. Uploads the single archive to S3 under
`FileFolder.GeneratedSdkClient`
6. Sets `isSdkLayerStale = true` on the `ApplicationEntity` in the
database
## 3. Invalidation Signal
The `isSdkLayerStale` boolean column on `ApplicationEntity` is the
invalidation mechanism:
- **Set to `true`** by `generateAndStore()` after uploading a new client
archive
- **Checked** by both logic function drivers before execution — if
`true`, they rebuild their local layer
- **Set back to `false`** by `markSdkLayerFresh()` after the driver has
successfully consumed the new archive
Default is `false` so existing applications without a generated client
aren't affected.
## 4a. Logic Functions — Local Driver
**`ensureSdkLayer()`** is called before every execution:
1. Checks if the local SDK layer directory exists AND `isSdkLayerStale`
is `false` → early return
2. Otherwise, cleans the local layer directory
3. Calls `downloadAndExtractToPackage()` which streams the zip from S3
directly to disk and extracts the full package into
`<tmpdir>/sdk/<workspaceId>-<appId>/node_modules/twenty-client-sdk/`
4. Calls `markSdkLayerFresh()` to set `isSdkLayerStale = false`
**At execution time**, `assembleNodeModules()` symlinks everything from
the deps layer's `node_modules/` **except** `twenty-client-sdk`, which
is symlinked from the SDK layer instead. This ensures the logic
function's `import ... from 'twenty-client-sdk/core'` resolves to the
generated client.
## 4b. Logic Functions — Lambda Driver
**`ensureSdkLayer()`** is called during `build()`:
1. Checks if `isSdkLayerStale` is `false` and an existing Lambda layer
ARN exists → early return
2. Otherwise, deletes all existing layer versions for this SDK layer
name
3. Calls `downloadArchiveBuffer()` to get the raw zip from S3 (no disk
extraction)
4. Calls `reprefixZipEntries()` which streams the zip entries into a
**new zip** with the path prefix
`nodejs/node_modules/twenty-client-sdk/` — this is the Lambda layer
convention path. All done in memory, no disk round-trip
5. Publishes the re-prefixed zip as a new Lambda layer via
`publishLayer()`
6. Calls `markSdkLayerFresh()`
**At function creation**, the Lambda is created with **two layers**:
`[depsLayerArn, sdkLayerArn]`. The SDK layer is listed last so it
overwrites the stub `twenty-client-sdk` from the deps layer (later
layers take precedence in Lambda's `/opt` merge).
## 5. Front Components
Front components are built by `app:build` with `twenty-client-sdk/core`
and `twenty-client-sdk/metadata` as **esbuild externals**. The stored
`.mjs` in S3 has unresolved bare import specifiers like `import {
CoreApiClient } from 'twenty-client-sdk/core'`.
SDK import resolution is split between the **frontend host** (fetching &
caching SDK modules) and the **Web Worker** (rewriting imports):
**Server endpoints**:
- `GET /rest/front-components/:id` —
`FrontComponentService.getBuiltComponentStream()` returns the **raw
`.mjs`** directly from file storage. No bundling, no SDK injection.
- `GET /rest/sdk-client/:applicationId/:moduleName` —
`SdkClientController` reads a single file (e.g. `dist/core.mjs`) from
the generated SDK archive via
`SdkClientGenerationService.readFileFromArchive()` and serves it as
JavaScript.
**Frontend host** (`FrontComponentRenderer` in `twenty-front`):
1. Queries `FindOneFrontComponent` which returns `applicationId`,
`builtComponentChecksum`, `usesSdkClient`, and `applicationTokenPair`
2. If `usesSdkClient` is `true`, renders
`FrontComponentRendererWithSdkClient` which calls the
`useApplicationSdkClient` hook
3. `useApplicationSdkClient({ applicationId, accessToken })` checks the
Jotai atom family cache for existing blob URLs. On cache miss, fetches
both SDK modules from `GET /rest/sdk-client/:applicationId/core` and
`/metadata`, creates **blob URLs** for each, and stores them in the atom
family
4. Once the blob URLs are cached, passes them as `sdkClientUrls`
(already blob URLs, not server URLs) to `SharedFrontComponentRenderer` →
`FrontComponentWorkerEffect` → worker's `render()` call via
`HostToWorkerRenderContext`
**Worker** (`remote-worker.ts` in `twenty-sdk`):
1. Fetches the raw component `.mjs` source as text
2. If `sdkClientUrls` are provided and the source contains SDK import
specifiers (`twenty-client-sdk/core`, `twenty-client-sdk/metadata`),
**rewrites** the bare specifiers to the blob URLs received from the host
(e.g. `'twenty-client-sdk/core'` → `'blob:...'`)
3. Creates a blob URL for the rewritten source and `import()`s it
4. Revokes only the component blob URL after the module is loaded — the
SDK blob URLs are owned and managed by the host's Jotai cache
This approach eliminates server-side esbuild bundling on every request,
caches SDK modules per application in the frontend, and keeps the
worker's job to a simple string rewrite.
## Summary Diagram
```
app:build (SDK)
└─ twenty-client-sdk stub (metadata=real, core=stub)
│
▼
WorkspaceMigrationRunnerService.run()
└─ SdkClientGenerationService.generateAndStore()
├─ Copy stub package (package.json + dist/)
├─ replaceCoreClient() → regenerate core.mjs/core.cjs
├─ Zip entire package → upload to S3
└─ Set isSdkLayerStale = true
│
┌────────┴────────────────────┐
▼ ▼
Logic Functions Front Components
│ │
├─ Local Driver ├─ GET /rest/sdk-client/:appId/core
│ └─ downloadAndExtract │ → core.mjs from archive
│ → symlink into │
│ node_modules ├─ Host (useApplicationSdkClient)
│ │ ├─ Fetch SDK modules
└─ Lambda Driver │ ├─ Create blob URLs
└─ downloadArchiveBuffer │ └─ Cache in Jotai atom family
→ reprefixZipEntries │
→ publish as Lambda ├─ GET /rest/front-components/:id
layer │ → raw .mjs (no bundling)
│
└─ Worker (browser)
├─ Fetch component .mjs
├─ Rewrite imports → blob URLs
└─ import() rewritten source
```
## Next PR
- Estimate perf improvement by implementing a redis caching for front
component client storage ( we don't even cache front comp initially )
- Implem frontent blob invalidation sse event from server
---------
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
parent
16451ee2ee
commit
4ea2e32366
189 changed files with 12487 additions and 9856 deletions
|
|
@ -3,12 +3,13 @@ description: >
|
|||
Starts a full Twenty instance (server, worker, database, redis) using Docker
|
||||
Compose. The server is available at http://localhost:3000 for subsequent steps
|
||||
in the caller's job.
|
||||
Pulls the specified semver image tag from Docker Hub.
|
||||
Accepts "latest" (pulls the latest Docker Hub image, checks out main) or a
|
||||
semver tag (e.g., v0.40.0).
|
||||
Designed to be consumed from external repositories (e.g., twenty-app).
|
||||
|
||||
inputs:
|
||||
twenty-version:
|
||||
description: 'Twenty Docker Hub image tag as semver (e.g., v0.40.0, v1.0.0).'
|
||||
description: 'Twenty Docker Hub image tag — either "latest" or a semver tag (e.g., v0.40.0).'
|
||||
required: true
|
||||
twenty-repository:
|
||||
description: 'Twenty repository to checkout docker compose files from.'
|
||||
|
|
@ -30,12 +31,19 @@ outputs:
|
|||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Validate version
|
||||
- name: Resolve version
|
||||
id: resolve
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION="${{ inputs.twenty-version }}"
|
||||
if ! echo "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error::twenty-version must be a semver tag (e.g., v0.40.0). Got: '$VERSION'"
|
||||
if [ "$VERSION" = "latest" ]; then
|
||||
echo "docker-tag=latest" >> "$GITHUB_OUTPUT"
|
||||
echo "git-ref=main" >> "$GITHUB_OUTPUT"
|
||||
elif echo "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "docker-tag=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "git-ref=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::error::twenty-version must be \"latest\" or a semver tag (e.g., v0.40.0). Got: '$VERSION'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -43,7 +51,7 @@ runs:
|
|||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ inputs.twenty-repository }}
|
||||
ref: ${{ inputs.twenty-version }}
|
||||
ref: ${{ steps.resolve.outputs.git-ref }}
|
||||
token: ${{ inputs.github-token }}
|
||||
sparse-checkout: |
|
||||
packages/twenty-docker
|
||||
|
|
@ -56,7 +64,7 @@ runs:
|
|||
run: |
|
||||
cp .env.example .env
|
||||
echo "" >> .env
|
||||
echo "TAG=${{ inputs.twenty-version }}" >> .env
|
||||
echo "TAG=${{ steps.resolve.outputs.docker-tag }}" >> .env
|
||||
echo "APP_SECRET=replace_me_with_a_random_string" >> .env
|
||||
echo "SERVER_URL=http://localhost:3000" >> .env
|
||||
|
||||
|
|
|
|||
3
.github/verdaccio-config.yaml
vendored
3
.github/verdaccio-config.yaml
vendored
|
|
@ -10,6 +10,9 @@ packages:
|
|||
'twenty-sdk':
|
||||
access: $all
|
||||
publish: $all
|
||||
'twenty-client-sdk':
|
||||
access: $all
|
||||
publish: $all
|
||||
'create-twenty-app':
|
||||
access: $all
|
||||
publish: $all
|
||||
|
|
|
|||
24
.github/workflows/ci-create-app-e2e.yaml
vendored
24
.github/workflows/ci-create-app-e2e.yaml
vendored
|
|
@ -21,10 +21,12 @@ jobs:
|
|||
files: |
|
||||
packages/create-twenty-app/**
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-client-sdk/**
|
||||
packages/twenty-shared/**
|
||||
packages/twenty-server/**
|
||||
!packages/create-twenty-app/package.json
|
||||
!packages/twenty-sdk/package.json
|
||||
!packages/twenty-client-sdk/package.json
|
||||
!packages/twenty-shared/package.json
|
||||
!packages/twenty-server/package.json
|
||||
create-app-e2e:
|
||||
|
|
@ -64,11 +66,12 @@ jobs:
|
|||
run: |
|
||||
CI_VERSION="0.0.0-ci.$(date +%s)"
|
||||
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
|
||||
npx nx run-many -t set-local-version -p twenty-sdk create-twenty-app --releaseVersion=$CI_VERSION
|
||||
npx nx run-many -t set-local-version -p twenty-sdk twenty-client-sdk create-twenty-app --releaseVersion=$CI_VERSION
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
npx nx build twenty-sdk
|
||||
npx nx build twenty-client-sdk
|
||||
npx nx build create-twenty-app
|
||||
|
||||
- name: Install and start Verdaccio
|
||||
|
|
@ -88,7 +91,7 @@ jobs:
|
|||
run: |
|
||||
npm set //localhost:4873/:_authToken "ci-auth-token"
|
||||
|
||||
for pkg in twenty-sdk create-twenty-app; do
|
||||
for pkg in twenty-sdk twenty-client-sdk create-twenty-app; do
|
||||
cd packages/$pkg
|
||||
npm publish --registry http://localhost:4873 --tag ci
|
||||
cd ../..
|
||||
|
|
@ -153,11 +156,15 @@ jobs:
|
|||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty remote add --token $SEED_API_KEY --url http://localhost:3000
|
||||
|
||||
- name: Build scaffolded app
|
||||
- name: Deploy scaffolded app
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty build
|
||||
test -d .twenty/output
|
||||
npx --no-install twenty deploy
|
||||
|
||||
- name: Install scaffolded app
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
npx --no-install twenty install
|
||||
|
||||
- name: Execute hello-world logic function
|
||||
run: |
|
||||
|
|
@ -166,6 +173,13 @@ jobs:
|
|||
echo "$EXEC_OUTPUT"
|
||||
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
|
||||
|
||||
- name: Execute create-hello-world-company logic function
|
||||
run: |
|
||||
cd /tmp/e2e-test-workspace/test-app
|
||||
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName create-hello-world-company)
|
||||
echo "$EXEC_OUTPUT"
|
||||
echo "$EXEC_OUTPUT" | grep -q "Created company"
|
||||
|
||||
- name: Run scaffolded app integration test
|
||||
env:
|
||||
TWENTY_API_URL: http://localhost:3000
|
||||
|
|
|
|||
10
.github/workflows/ci-server.yaml
vendored
10
.github/workflows/ci-server.yaml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
packages/twenty-server/**
|
||||
packages/twenty-front/src/generated/**
|
||||
packages/twenty-front/src/generated-metadata/**
|
||||
packages/twenty-sdk/src/clients/generated/metadata/**
|
||||
packages/twenty-client-sdk/**
|
||||
packages/twenty-emails/**
|
||||
packages/twenty-shared/**
|
||||
|
||||
|
|
@ -177,14 +177,14 @@ jobs:
|
|||
HAS_ERRORS=true
|
||||
fi
|
||||
|
||||
npx nx run twenty-sdk:generate-metadata-client
|
||||
npx nx run twenty-client-sdk:generate-metadata-client
|
||||
|
||||
if ! git diff --quiet -- packages/twenty-sdk/src/clients/generated/metadata; then
|
||||
echo "::error::SDK metadata client changes detected. Please run 'npx nx run twenty-sdk:generate-metadata-client' and commit the changes."
|
||||
if ! git diff --quiet -- packages/twenty-client-sdk/src/metadata/generated; then
|
||||
echo "::error::SDK metadata client changes detected. Please run 'npx nx run twenty-client-sdk:generate-metadata-client' and commit the changes."
|
||||
echo ""
|
||||
echo "The following SDK metadata client changes were detected:"
|
||||
echo "==================================================="
|
||||
git diff -- packages/twenty-sdk/src/clients/generated/metadata
|
||||
git diff -- packages/twenty-client-sdk/src/metadata/generated
|
||||
echo "==================================================="
|
||||
echo ""
|
||||
HAS_ERRORS=true
|
||||
|
|
|
|||
|
|
@ -207,6 +207,7 @@
|
|||
"packages/twenty-e2e-testing",
|
||||
"packages/twenty-shared",
|
||||
"packages/twenty-sdk",
|
||||
"packages/twenty-client-sdk",
|
||||
"packages/twenty-apps",
|
||||
"packages/twenty-cli",
|
||||
"packages/create-twenty-app",
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ yarn twenty server start # Start local Twenty server
|
|||
yarn twenty remote add --local # Authenticate via OAuth
|
||||
|
||||
# Start dev mode: watches, builds, and syncs local changes to your workspace
|
||||
# (also auto-generates typed CoreApiClient — MetadataApiClient ships pre-built — both available via `twenty-client-sdk`)
|
||||
yarn twenty dev
|
||||
|
||||
# Watch your application's function logs
|
||||
|
|
@ -135,7 +136,7 @@ npx create-twenty-app@latest my-app --port 3000
|
|||
- Use `yarn twenty remote add --local` to authenticate with your Twenty workspace via OAuth.
|
||||
- Explore the generated project and add your first entity with `yarn twenty add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
|
||||
- Use `yarn twenty dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
- `CoreApiClient` (for workspace data via `/graphql`) is auto-generated by `yarn twenty dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient, MetadataApiClient } from 'twenty-sdk/clients'`.
|
||||
- `CoreApiClient` is auto-generated by `yarn twenty dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient } from 'twenty-client-sdk/core'` and `import { MetadataApiClient } from 'twenty-client-sdk/metadata'`.
|
||||
|
||||
## Build and publish your application
|
||||
|
||||
|
|
|
|||
|
|
@ -106,6 +106,9 @@ describe('copyBaseApplicationProject', () => {
|
|||
expect(packageJson.devDependencies['twenty-sdk']).toBe(
|
||||
createTwentyAppPackageJson.version,
|
||||
);
|
||||
expect(packageJson.devDependencies['twenty-client-sdk']).toBe(
|
||||
createTwentyAppPackageJson.version,
|
||||
);
|
||||
expect(packageJson.scripts['twenty']).toBe('twenty');
|
||||
});
|
||||
|
||||
|
|
@ -362,11 +365,21 @@ describe('copyBaseApplicationProject', () => {
|
|||
join(srcPath, 'logic-functions', 'hello-world.ts'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'logic-functions', 'create-hello-world-company.ts'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'front-components', 'hello-world.tsx'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'page-layouts', 'example-record-page-layout.ts'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(join(srcPath, 'views', 'example-view.ts')),
|
||||
).toBe(true);
|
||||
|
|
@ -448,11 +461,21 @@ describe('copyBaseApplicationProject', () => {
|
|||
join(srcPath, 'logic-functions', 'hello-world.ts'),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'logic-functions', 'create-hello-world-company.ts'),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'front-components', 'hello-world.tsx'),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'page-layouts', 'example-record-page-layout.ts'),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await fs.pathExists(join(srcPath, 'views', 'example-view.ts')),
|
||||
).toBe(false);
|
||||
|
|
@ -480,7 +503,7 @@ describe('copyBaseApplicationProject', () => {
|
|||
});
|
||||
|
||||
describe('selective examples', () => {
|
||||
it('should create only front component when only that option is enabled', async () => {
|
||||
it('should create front component and page layout when only front component option is enabled', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
|
|
@ -506,6 +529,11 @@ describe('copyBaseApplicationProject', () => {
|
|||
join(srcPath, 'front-components', 'hello-world.tsx'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'page-layouts', 'example-record-page-layout.ts'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(join(srcPath, 'objects', 'example-object.ts')),
|
||||
).toBe(false);
|
||||
|
|
@ -545,6 +573,11 @@ describe('copyBaseApplicationProject', () => {
|
|||
join(srcPath, 'logic-functions', 'hello-world.ts'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'logic-functions', 'create-hello-world-company.ts'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(join(srcPath, 'objects', 'example-object.ts')),
|
||||
).toBe(false);
|
||||
|
|
|
|||
|
|
@ -47,15 +47,17 @@ describe('scaffoldIntegrationTest', () => {
|
|||
const content = await fs.readFile(testPath, 'utf8');
|
||||
|
||||
expect(content).toContain(
|
||||
"import { appBuild, appUninstall } from 'twenty-sdk/cli'",
|
||||
"import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'",
|
||||
);
|
||||
expect(content).toContain(
|
||||
"import { MetadataApiClient } from 'twenty-sdk/clients'",
|
||||
"import { MetadataApiClient } from 'twenty-client-sdk/metadata'",
|
||||
);
|
||||
expect(content).toContain(
|
||||
"import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'",
|
||||
);
|
||||
expect(content).toContain('appBuild');
|
||||
expect(content).toContain('appDeploy');
|
||||
expect(content).toContain('appInstall');
|
||||
expect(content).toContain('appUninstall');
|
||||
expect(content).toContain('new MetadataApiClient()');
|
||||
expect(content).toContain('findManyApplications');
|
||||
|
|
@ -136,7 +138,7 @@ describe('scaffoldIntegrationTest', () => {
|
|||
expect(content).toContain('yarn install --immutable');
|
||||
expect(content).toContain('yarn test');
|
||||
expect(content).toContain('TWENTY_API_URL');
|
||||
expect(content).toContain('TWENTY_TEST_API_KEY');
|
||||
expect(content).toContain('TWENTY_API_KEY');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ export const copyBaseApplicationProject = async ({
|
|||
|
||||
await createGitignore(appDirectory);
|
||||
|
||||
await createNvmrc(appDirectory);
|
||||
|
||||
await createPublicAssetDirectory(appDirectory);
|
||||
|
||||
const sourceFolderPath = join(appDirectory, SRC_FOLDER);
|
||||
|
|
@ -69,6 +71,12 @@ export const copyBaseApplicationProject = async ({
|
|||
fileFolder: 'logic-functions',
|
||||
fileName: 'hello-world.ts',
|
||||
});
|
||||
|
||||
await createCreateCompanyFunction({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'logic-functions',
|
||||
fileName: 'create-hello-world-company.ts',
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleOptions.includeExampleFrontComponent) {
|
||||
|
|
@ -77,6 +85,12 @@ export const copyBaseApplicationProject = async ({
|
|||
fileFolder: 'front-components',
|
||||
fileName: 'hello-world.tsx',
|
||||
});
|
||||
|
||||
await createExamplePageLayout({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'page-layouts',
|
||||
fileName: 'example-record-page-layout.ts',
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleOptions.includeExampleView) {
|
||||
|
|
@ -186,6 +200,10 @@ yarn-error.log*
|
|||
await fs.writeFile(join(appDirectory, '.gitignore'), gitignoreContent);
|
||||
};
|
||||
|
||||
const createNvmrc = async (appDirectory: string) => {
|
||||
await fs.writeFile(join(appDirectory, '.nvmrc'), '24.5.0\n');
|
||||
};
|
||||
|
||||
const createDefaultRoleConfig = async ({
|
||||
displayName,
|
||||
appDirectory,
|
||||
|
|
@ -230,19 +248,59 @@ const createDefaultFrontComponent = async ({
|
|||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineFrontComponent } from 'twenty-sdk';
|
||||
const content = `import { useEffect, useState } from 'react';
|
||||
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
|
||||
export const HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
|
||||
'${universalIdentifier}';
|
||||
|
||||
export const HelloWorld = () => {
|
||||
const client = new CoreApiClient();
|
||||
const [data, setData] = useState<
|
||||
Pick<CoreSchema.Company, 'name' | 'id'> | undefined
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const response = await client.query({
|
||||
company: {
|
||||
name: true,
|
||||
id: true,
|
||||
__args: {
|
||||
filter: {
|
||||
position: {
|
||||
eq: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setData(response.company);
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>Hello, World!</h1>
|
||||
<p>This is your first front component.</p>
|
||||
{data ? (
|
||||
<div>
|
||||
<p>Company name: {data.name}</p>
|
||||
<p>Company id: {data.id}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p>Company not found</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
universalIdentifier: HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'hello-world-front-component',
|
||||
description: 'A sample front component',
|
||||
component: HelloWorld,
|
||||
|
|
@ -253,6 +311,56 @@ export default defineFrontComponent({
|
|||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createExamplePageLayout = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const pageLayoutUniversalIdentifier = v4();
|
||||
const tabUniversalIdentifier = v4();
|
||||
const widgetUniversalIdentifier = v4();
|
||||
|
||||
const content = `import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/front-components/hello-world';
|
||||
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk';
|
||||
|
||||
export default definePageLayout({
|
||||
universalIdentifier: '${pageLayoutUniversalIdentifier}',
|
||||
name: 'Example Record Page',
|
||||
type: 'RECORD_PAGE',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
tabs: [
|
||||
{
|
||||
universalIdentifier: '${tabUniversalIdentifier}',
|
||||
title: 'Hello World',
|
||||
position: 50,
|
||||
icon: 'IconWorld',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: '${widgetUniversalIdentifier}',
|
||||
title: 'Hello World',
|
||||
type: 'FRONT_COMPONENT',
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier:
|
||||
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createDefaultFunction = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
|
|
@ -288,6 +396,58 @@ export default defineLogicFunction({
|
|||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createCreateCompanyFunction = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
|
||||
const handler = async (): Promise<{ message: string }> => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const { createCompany } = await client.mutation({
|
||||
createCompany: {
|
||||
__args: {
|
||||
data: {
|
||||
name: 'Hello World',
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
message: \`Created company "\${createCompany?.name}" with id \${createCompany?.id}\`,
|
||||
};
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
name: 'create-hello-world-company',
|
||||
description: 'Creates a company called Hello World',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/create-hello-world-company',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createDefaultPreInstallFunction = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
|
|
@ -615,6 +775,7 @@ const createPackageJson = async ({
|
|||
'react-dom': '^19.0.0',
|
||||
oxlint: '^0.16.0',
|
||||
'twenty-sdk': createTwentyAppPackageJson.version,
|
||||
'twenty-client-sdk': createTwentyAppPackageJson.version,
|
||||
};
|
||||
|
||||
if (includeExampleIntegrationTest) {
|
||||
|
|
|
|||
|
|
@ -149,18 +149,17 @@ const createIntegrationTest = async ({
|
|||
fileName: string;
|
||||
}) => {
|
||||
const content = `import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
|
||||
import { appBuild, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-sdk/clients';
|
||||
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', () => {
|
||||
let appInstalled = false;
|
||||
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
tarball: true,
|
||||
onProgress: (message: string) => console.log(\`[build] \${message}\`),
|
||||
});
|
||||
|
||||
|
|
@ -170,14 +169,27 @@ describe('App installation', () => {
|
|||
);
|
||||
}
|
||||
|
||||
appInstalled = true;
|
||||
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 () => {
|
||||
if (!appInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
|
|
@ -257,7 +269,7 @@ jobs:
|
|||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: \${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_TEST_API_KEY: \${{ steps.twenty.outputs.access-token }}
|
||||
TWENTY_API_KEY: \${{ steps.twenty.outputs.access-token }}
|
||||
`;
|
||||
|
||||
const workflowDir = join(appDirectory, '.github', 'workflows');
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
|||
|
||||
export default defineField({
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: '8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e',
|
||||
universalIdentifier: 'b602dbd9-e511-49ce-b6d3-b697218dc69c',
|
||||
type: FieldType.SELECT,
|
||||
name: 'category',
|
||||
label: 'Category',
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
|||
|
||||
export default defineField({
|
||||
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d',
|
||||
universalIdentifier: '7b57bd63-5a4c-46ca-9d52-42c8f02d1df6',
|
||||
type: FieldType.NUMBER,
|
||||
name: 'priority',
|
||||
label: 'Priority',
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ export default defineObject({
|
|||
{
|
||||
universalIdentifier: 'd3a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
type: FieldType.ADDRESS,
|
||||
label: 'Address',
|
||||
name: 'address',
|
||||
label: 'Mailing Address',
|
||||
name: 'mailingAddress',
|
||||
icon: 'IconHome',
|
||||
},
|
||||
],
|
||||
|
|
|
|||
42
packages/twenty-apps/hello-world/.github/workflows/ci.yml
vendored
Normal file
42
packages/twenty-apps/hello-world/.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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.access-token }}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/app-seeds/rich-app
|
||||
|
||||
## UUID requirement
|
||||
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@
|
|||
"@types/react": "^18.2.0",
|
||||
"oxlint": "^0.16.0",
|
||||
"react": "^18.2.0",
|
||||
"twenty-sdk": "0.6.4",
|
||||
"twenty-client-sdk": "portal:../../twenty-client-sdk",
|
||||
"twenty-sdk": "portal:../../twenty-sdk",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^3.1.1"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
|
||||
import { appBuild, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-sdk/clients';
|
||||
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', () => {
|
||||
let appInstalled = false;
|
||||
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
tarball: true,
|
||||
onProgress: (message: string) => console.log(`[build] ${message}`),
|
||||
});
|
||||
|
||||
|
|
@ -20,14 +19,27 @@ describe('App installation', () => {
|
|||
);
|
||||
}
|
||||
|
||||
appInstalled = true;
|
||||
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 () => {
|
||||
if (!appInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
|
|
|
|||
13
packages/twenty-apps/hello-world/src/agents/example-agent.ts
Normal file
13
packages/twenty-apps/hello-world/src/agents/example-agent.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { defineAgent } from 'twenty-sdk';
|
||||
|
||||
export const EXAMPLE_AGENT_UNIVERSAL_IDENTIFIER =
|
||||
'110bebc2-f116-46b6-a35d-61e91c3c0a43';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: EXAMPLE_AGENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'example-agent',
|
||||
label: 'Example Agent',
|
||||
description: 'A sample AI agent for your application',
|
||||
icon: 'IconRobot',
|
||||
prompt: 'You are a helpful assistant. Help users with their questions and tasks.',
|
||||
});
|
||||
|
|
@ -2,7 +2,7 @@ import { defineApplication } from 'twenty-sdk';
|
|||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
|
||||
export const APPLICATION_UNIVERSAL_IDENTIFIER =
|
||||
'6563e091-9f5b-4026-a3ea-7e3b3d09e218';
|
||||
'bb1decf6-dee5-43ef-b881-9799f97b02a8';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object'
|
|||
|
||||
export default defineField({
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: '770d32c2-cf12-4ab2-b66d-73f92dc239b5',
|
||||
universalIdentifier: 'be08a7c6-2586-4d91-9fa7-0a44e6eae30c',
|
||||
type: FieldType.NUMBER,
|
||||
name: 'priority',
|
||||
label: 'Priority',
|
||||
|
|
|
|||
|
|
@ -1,16 +1,56 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
|
||||
export const HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
|
||||
'7a758f23-5e7d-497d-98c9-7ca8d6c085b0';
|
||||
|
||||
export const HelloWorld = () => {
|
||||
const client = new CoreApiClient();
|
||||
const [data, setData] = useState<
|
||||
Pick<CoreSchema.Company, 'name' | 'id'> | undefined
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const response = await client.query({
|
||||
company: {
|
||||
name: true,
|
||||
id: true,
|
||||
__args: {
|
||||
filter: {
|
||||
position: {
|
||||
eq: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setData(response.company);
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>Hello, World!</h1>
|
||||
<p>This is your first front component.</p>
|
||||
{data ? (
|
||||
<div>
|
||||
<p>Company name: {data.name}</p>
|
||||
<p>Company id: {data.id}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p>Company not found</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: 'd371f098-5b2c-42f0-898d-94459f1ee337',
|
||||
universalIdentifier: HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'hello-world-front-component',
|
||||
description: 'A sample front component',
|
||||
component: HelloWorld,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
|
||||
const handler = async (): Promise<{ message: string }> => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const { createCompany } = await client.mutation({
|
||||
createCompany: {
|
||||
__args: {
|
||||
data: {
|
||||
name: 'Hello World',
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
message: `Created company "${createCompany?.name}" with id ${createCompany?.id}`,
|
||||
};
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '94abaa53-d265-4fa4-ae52-1d7ea711ecf2',
|
||||
name: 'create-hello-world-company',
|
||||
description: 'Creates a company called Hello World',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/create-hello-world-company',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -5,7 +5,7 @@ const handler = async (): Promise<{ message: string }> => {
|
|||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '2baa26eb-9aaf-4856-a4f4-30d6fd6480ee',
|
||||
universalIdentifier: 'b05e4b30-72d4-4d7f-8091-32e037b601da',
|
||||
name: 'hello-world-logic-function',
|
||||
description: 'A simple logic function',
|
||||
timeoutSeconds: 5,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
|||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: '7a3f4684-51db-494d-833b-a747a3b90507',
|
||||
universalIdentifier: '8c726dcc-1709-4eac-aa8b-f99960a9ec1b',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
|
|||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: '1272ffdb-8e2f-492c-ab37-66c2b97e9c23',
|
||||
universalIdentifier: 'f8ad4b09-6a12-4b12-a52a-3472d3a78dc7',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { NavigationMenuItemType } from 'twenty-shared/types';
|
|||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '10f90627-e9c2-44b7-9742-bed77e3d1b17',
|
||||
universalIdentifier: '9327db91-afa1-41b6-bd9d-2b51a26efb4c',
|
||||
name: 'example-navigation-menu-item',
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { defineObject, FieldType } from 'twenty-sdk';
|
||||
|
||||
export const EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
'dfd43356-39b3-4b55-b4a7-279bec689928';
|
||||
'47fd9bd9-392b-4d9f-9091-9a91b1edf519';
|
||||
|
||||
export const NAME_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd2d7f6cd-33f6-456f-bf00-17adeca926ba';
|
||||
'2d9ff841-cf8e-44ec-ad8e-468455f7eebd';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
|
||||
import { HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/front-components/hello-world';
|
||||
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk';
|
||||
|
||||
export default definePageLayout({
|
||||
universalIdentifier: '203aeb94-6701-46d6-9af1-be2bbcc9e134',
|
||||
name: 'Example Record Page',
|
||||
type: 'RECORD_PAGE',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
tabs: [
|
||||
{
|
||||
universalIdentifier: '6ed26b60-a51d-4ad7-86dd-1c04c7f3cac5',
|
||||
title: 'Hello World',
|
||||
position: 50,
|
||||
icon: 'IconWorld',
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [
|
||||
{
|
||||
universalIdentifier: 'aa4234e0-2e5f-4c02-a96a-573449e2351d',
|
||||
title: 'Hello World',
|
||||
type: 'FRONT_COMPONENT',
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier:
|
||||
HELLO_WORLD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { defineRole } from 'twenty-sdk';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'9238bc7b-d38f-4a1c-9d19-31ab7bc67a2f';
|
||||
'c38f4d11-760c-4d5c-89ed-e569c28b7b70';
|
||||
|
||||
export default defineRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { defineSkill } from 'twenty-sdk';
|
||||
|
||||
export const EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER =
|
||||
'd0940029-9d3c-40be-903a-52d65393028f';
|
||||
'90cf9144-4811-4653-93a2-9a6780fe6aac';
|
||||
|
||||
export default defineSkill({
|
||||
universalIdentifier: EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { defineView, ViewKey } from 'twenty-sdk';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER, NAME_FIELD_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
|
||||
|
||||
export const EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER = 'e004df40-29f3-47ba-b39d-d3a5c444367a';
|
||||
export const EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER = '965e3776-b966-4be8-83f7-6cd3bce5e1bd';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
|
|
@ -12,7 +12,7 @@ export default defineView({
|
|||
position: 0,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '496c40c2-5766-419c-93bf-20fdad3f34bb',
|
||||
universalIdentifier: 'f926bdb7-6af7-4683-9a09-adbca56c29f0',
|
||||
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
|
|
|
|||
|
|
@ -352,9 +352,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/aix-ppc64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/aix-ppc64@npm:0.27.3"
|
||||
"@esbuild/aix-ppc64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/aix-ppc64@npm:0.27.4"
|
||||
conditions: os=aix & cpu=ppc64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -366,9 +366,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-arm64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/android-arm64@npm:0.27.3"
|
||||
"@esbuild/android-arm64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/android-arm64@npm:0.27.4"
|
||||
conditions: os=android & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -380,9 +380,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-arm@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/android-arm@npm:0.27.3"
|
||||
"@esbuild/android-arm@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/android-arm@npm:0.27.4"
|
||||
conditions: os=android & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -394,9 +394,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-x64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/android-x64@npm:0.27.3"
|
||||
"@esbuild/android-x64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/android-x64@npm:0.27.4"
|
||||
conditions: os=android & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -408,9 +408,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/darwin-arm64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/darwin-arm64@npm:0.27.3"
|
||||
"@esbuild/darwin-arm64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/darwin-arm64@npm:0.27.4"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -422,9 +422,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/darwin-x64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/darwin-x64@npm:0.27.3"
|
||||
"@esbuild/darwin-x64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/darwin-x64@npm:0.27.4"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -436,9 +436,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/freebsd-arm64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/freebsd-arm64@npm:0.27.3"
|
||||
"@esbuild/freebsd-arm64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/freebsd-arm64@npm:0.27.4"
|
||||
conditions: os=freebsd & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -450,9 +450,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/freebsd-x64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/freebsd-x64@npm:0.27.3"
|
||||
"@esbuild/freebsd-x64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/freebsd-x64@npm:0.27.4"
|
||||
conditions: os=freebsd & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -464,9 +464,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-arm64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/linux-arm64@npm:0.27.3"
|
||||
"@esbuild/linux-arm64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/linux-arm64@npm:0.27.4"
|
||||
conditions: os=linux & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -478,9 +478,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-arm@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/linux-arm@npm:0.27.3"
|
||||
"@esbuild/linux-arm@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/linux-arm@npm:0.27.4"
|
||||
conditions: os=linux & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -492,9 +492,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-ia32@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/linux-ia32@npm:0.27.3"
|
||||
"@esbuild/linux-ia32@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/linux-ia32@npm:0.27.4"
|
||||
conditions: os=linux & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -506,9 +506,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-loong64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/linux-loong64@npm:0.27.3"
|
||||
"@esbuild/linux-loong64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/linux-loong64@npm:0.27.4"
|
||||
conditions: os=linux & cpu=loong64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -520,9 +520,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-mips64el@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/linux-mips64el@npm:0.27.3"
|
||||
"@esbuild/linux-mips64el@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/linux-mips64el@npm:0.27.4"
|
||||
conditions: os=linux & cpu=mips64el
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -534,9 +534,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-ppc64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/linux-ppc64@npm:0.27.3"
|
||||
"@esbuild/linux-ppc64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/linux-ppc64@npm:0.27.4"
|
||||
conditions: os=linux & cpu=ppc64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -548,9 +548,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-riscv64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/linux-riscv64@npm:0.27.3"
|
||||
"@esbuild/linux-riscv64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/linux-riscv64@npm:0.27.4"
|
||||
conditions: os=linux & cpu=riscv64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -562,9 +562,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-s390x@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/linux-s390x@npm:0.27.3"
|
||||
"@esbuild/linux-s390x@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/linux-s390x@npm:0.27.4"
|
||||
conditions: os=linux & cpu=s390x
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -576,9 +576,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-x64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/linux-x64@npm:0.27.3"
|
||||
"@esbuild/linux-x64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/linux-x64@npm:0.27.4"
|
||||
conditions: os=linux & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -590,9 +590,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/netbsd-arm64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/netbsd-arm64@npm:0.27.3"
|
||||
"@esbuild/netbsd-arm64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/netbsd-arm64@npm:0.27.4"
|
||||
conditions: os=netbsd & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -604,9 +604,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/netbsd-x64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/netbsd-x64@npm:0.27.3"
|
||||
"@esbuild/netbsd-x64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/netbsd-x64@npm:0.27.4"
|
||||
conditions: os=netbsd & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -618,9 +618,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/openbsd-arm64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/openbsd-arm64@npm:0.27.3"
|
||||
"@esbuild/openbsd-arm64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/openbsd-arm64@npm:0.27.4"
|
||||
conditions: os=openbsd & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -632,9 +632,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/openbsd-x64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/openbsd-x64@npm:0.27.3"
|
||||
"@esbuild/openbsd-x64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/openbsd-x64@npm:0.27.4"
|
||||
conditions: os=openbsd & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -646,9 +646,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/openharmony-arm64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/openharmony-arm64@npm:0.27.3"
|
||||
"@esbuild/openharmony-arm64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/openharmony-arm64@npm:0.27.4"
|
||||
conditions: os=openharmony & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -660,9 +660,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/sunos-x64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/sunos-x64@npm:0.27.3"
|
||||
"@esbuild/sunos-x64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/sunos-x64@npm:0.27.4"
|
||||
conditions: os=sunos & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -674,9 +674,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-arm64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/win32-arm64@npm:0.27.3"
|
||||
"@esbuild/win32-arm64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/win32-arm64@npm:0.27.4"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -688,9 +688,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-ia32@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/win32-ia32@npm:0.27.3"
|
||||
"@esbuild/win32-ia32@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/win32-ia32@npm:0.27.4"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -702,9 +702,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-x64@npm:0.27.3":
|
||||
version: 0.27.3
|
||||
resolution: "@esbuild/win32-x64@npm:0.27.3"
|
||||
"@esbuild/win32-x64@npm:0.27.4":
|
||||
version: 0.27.4
|
||||
resolution: "@esbuild/win32-x64@npm:0.27.4"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
|
@ -1541,11 +1541,11 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"@types/node@npm:*":
|
||||
version: 25.3.5
|
||||
resolution: "@types/node@npm:25.3.5"
|
||||
version: 25.5.0
|
||||
resolution: "@types/node@npm:25.5.0"
|
||||
dependencies:
|
||||
undici-types: "npm:~7.18.0"
|
||||
checksum: 10c0/4cf0834a6f6933bf0aca6afead117ae3db3b8f02a5f7187a24f871db0fb9344e5e46573ba387bc53b7505e1e219c4c535cbe67221ced95bb5ad98573223b19d0
|
||||
checksum: 10c0/70c508165b6758c4f88d4f91abca526c3985eee1985503d4c2bd994dbaf588e52ac57e571160f18f117d76e963570ac82bd20e743c18987e82564312b3b62119
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3408,35 +3408,35 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"esbuild@npm:^0.27.0":
|
||||
version: 0.27.3
|
||||
resolution: "esbuild@npm:0.27.3"
|
||||
version: 0.27.4
|
||||
resolution: "esbuild@npm:0.27.4"
|
||||
dependencies:
|
||||
"@esbuild/aix-ppc64": "npm:0.27.3"
|
||||
"@esbuild/android-arm": "npm:0.27.3"
|
||||
"@esbuild/android-arm64": "npm:0.27.3"
|
||||
"@esbuild/android-x64": "npm:0.27.3"
|
||||
"@esbuild/darwin-arm64": "npm:0.27.3"
|
||||
"@esbuild/darwin-x64": "npm:0.27.3"
|
||||
"@esbuild/freebsd-arm64": "npm:0.27.3"
|
||||
"@esbuild/freebsd-x64": "npm:0.27.3"
|
||||
"@esbuild/linux-arm": "npm:0.27.3"
|
||||
"@esbuild/linux-arm64": "npm:0.27.3"
|
||||
"@esbuild/linux-ia32": "npm:0.27.3"
|
||||
"@esbuild/linux-loong64": "npm:0.27.3"
|
||||
"@esbuild/linux-mips64el": "npm:0.27.3"
|
||||
"@esbuild/linux-ppc64": "npm:0.27.3"
|
||||
"@esbuild/linux-riscv64": "npm:0.27.3"
|
||||
"@esbuild/linux-s390x": "npm:0.27.3"
|
||||
"@esbuild/linux-x64": "npm:0.27.3"
|
||||
"@esbuild/netbsd-arm64": "npm:0.27.3"
|
||||
"@esbuild/netbsd-x64": "npm:0.27.3"
|
||||
"@esbuild/openbsd-arm64": "npm:0.27.3"
|
||||
"@esbuild/openbsd-x64": "npm:0.27.3"
|
||||
"@esbuild/openharmony-arm64": "npm:0.27.3"
|
||||
"@esbuild/sunos-x64": "npm:0.27.3"
|
||||
"@esbuild/win32-arm64": "npm:0.27.3"
|
||||
"@esbuild/win32-ia32": "npm:0.27.3"
|
||||
"@esbuild/win32-x64": "npm:0.27.3"
|
||||
"@esbuild/aix-ppc64": "npm:0.27.4"
|
||||
"@esbuild/android-arm": "npm:0.27.4"
|
||||
"@esbuild/android-arm64": "npm:0.27.4"
|
||||
"@esbuild/android-x64": "npm:0.27.4"
|
||||
"@esbuild/darwin-arm64": "npm:0.27.4"
|
||||
"@esbuild/darwin-x64": "npm:0.27.4"
|
||||
"@esbuild/freebsd-arm64": "npm:0.27.4"
|
||||
"@esbuild/freebsd-x64": "npm:0.27.4"
|
||||
"@esbuild/linux-arm": "npm:0.27.4"
|
||||
"@esbuild/linux-arm64": "npm:0.27.4"
|
||||
"@esbuild/linux-ia32": "npm:0.27.4"
|
||||
"@esbuild/linux-loong64": "npm:0.27.4"
|
||||
"@esbuild/linux-mips64el": "npm:0.27.4"
|
||||
"@esbuild/linux-ppc64": "npm:0.27.4"
|
||||
"@esbuild/linux-riscv64": "npm:0.27.4"
|
||||
"@esbuild/linux-s390x": "npm:0.27.4"
|
||||
"@esbuild/linux-x64": "npm:0.27.4"
|
||||
"@esbuild/netbsd-arm64": "npm:0.27.4"
|
||||
"@esbuild/netbsd-x64": "npm:0.27.4"
|
||||
"@esbuild/openbsd-arm64": "npm:0.27.4"
|
||||
"@esbuild/openbsd-x64": "npm:0.27.4"
|
||||
"@esbuild/openharmony-arm64": "npm:0.27.4"
|
||||
"@esbuild/sunos-x64": "npm:0.27.4"
|
||||
"@esbuild/win32-arm64": "npm:0.27.4"
|
||||
"@esbuild/win32-ia32": "npm:0.27.4"
|
||||
"@esbuild/win32-x64": "npm:0.27.4"
|
||||
dependenciesMeta:
|
||||
"@esbuild/aix-ppc64":
|
||||
optional: true
|
||||
|
|
@ -3492,7 +3492,7 @@ __metadata:
|
|||
optional: true
|
||||
bin:
|
||||
esbuild: bin/esbuild
|
||||
checksum: 10c0/fdc3f87a3f08b3ef98362f37377136c389a0d180fda4b8d073b26ba930cf245521db0a368f119cc7624bc619248fff1439f5811f062d853576f8ffa3df8ee5f1
|
||||
checksum: 10c0/2a1c2bcccda279f2afd72a7f8259860cb4483b32453d17878e1ecb4ac416b9e7c1001e7aa0a25ba4c29c1e250a3ceaae5d8bb72a119815bc8db4e9b5f5321490
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3906,6 +3906,7 @@ __metadata:
|
|||
"@types/react": "npm:^18.2.0"
|
||||
oxlint: "npm:^0.16.0"
|
||||
react: "npm:^18.2.0"
|
||||
twenty-client-sdk: "portal:../../twenty-client-sdk"
|
||||
twenty-sdk: "portal:../../twenty-sdk"
|
||||
typescript: "npm:^5.9.3"
|
||||
vite-tsconfig-paths: "npm:^4.2.1"
|
||||
|
|
@ -4952,9 +4953,9 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"preact@npm:^10.28.3":
|
||||
version: 10.28.4
|
||||
resolution: "preact@npm:10.28.4"
|
||||
checksum: 10c0/712384e92d6c676c8f4c230eab81329a66acfecf700390aebfe5fc46168fa3686345d47f2292068dcc0a6d86425a3d9a3d743730e356e867a8402442afb989db
|
||||
version: 10.29.0
|
||||
resolution: "preact@npm:10.29.0"
|
||||
checksum: 10c0/d111381e5b48335e3a797a03adb83521cf5e9bdf880570fb2eff4fe9da9c82e6dedcbdf54538b1ed8f60bf813a0df0f4891b03dc32140ad93f8f720a8812dd5c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -5750,6 +5751,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"twenty-client-sdk@portal:../../twenty-client-sdk::locator=hello-world%40workspace%3A.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "twenty-client-sdk@portal:../../twenty-client-sdk::locator=hello-world%40workspace%3A."
|
||||
dependencies:
|
||||
"@genql/cli": "npm:^3.0.3"
|
||||
"@genql/runtime": "npm:^2.10.0"
|
||||
esbuild: "npm:^0.25.0"
|
||||
graphql: "npm:^16.8.1"
|
||||
languageName: node
|
||||
linkType: soft
|
||||
|
||||
"twenty-sdk@portal:../../twenty-sdk::locator=hello-world%40workspace%3A.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "twenty-sdk@portal:../../twenty-sdk::locator=hello-world%40workspace%3A."
|
||||
|
|
|
|||
47
packages/twenty-client-sdk/.oxlintrc.json
Normal file
47
packages/twenty-client-sdk/.oxlintrc.json
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript", "import", "unicorn"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist", "generated"],
|
||||
"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": "^_"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
2
packages/twenty-client-sdk/.prettierignore
Normal file
2
packages/twenty-client-sdk/.prettierignore
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
dist
|
||||
generated
|
||||
60
packages/twenty-client-sdk/package.json
Normal file
60
packages/twenty-client-sdk/package.json
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"version": "0.7.0-canary.0",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"build": "npx rimraf dist && npx vite build && tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist"
|
||||
},
|
||||
"exports": {
|
||||
"./core": {
|
||||
"types": "./dist/core/index.d.ts",
|
||||
"import": "./dist/core.mjs",
|
||||
"require": "./dist/core.cjs"
|
||||
},
|
||||
"./metadata": {
|
||||
"types": "./dist/metadata/index.d.ts",
|
||||
"import": "./dist/metadata.mjs",
|
||||
"require": "./dist/metadata.cjs"
|
||||
},
|
||||
"./generate": {
|
||||
"types": "./dist/generate/index.d.ts",
|
||||
"import": "./dist/generate.mjs",
|
||||
"require": "./dist/generate.cjs"
|
||||
}
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"core": [
|
||||
"dist/core/index.d.ts"
|
||||
],
|
||||
"metadata": [
|
||||
"dist/metadata/index.d.ts"
|
||||
],
|
||||
"generate": [
|
||||
"dist/generate/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@genql/cli": "^3.0.3",
|
||||
"@genql/runtime": "^2.10.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"graphql": "^16.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"twenty-shared": "workspace:*",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"yarn": "^4.0.2"
|
||||
}
|
||||
}
|
||||
44
packages/twenty-client-sdk/project.json
Normal file
44
packages/twenty-client-sdk/project.json
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "packages/twenty-client-sdk/src",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:sdk"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"inputs": ["production", "^production"],
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["{projectRoot}/dist"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"commands": [
|
||||
"npx rimraf dist && npx vite build",
|
||||
"tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist"
|
||||
],
|
||||
"parallel": false
|
||||
}
|
||||
},
|
||||
"generate-metadata-client": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": false,
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["{projectRoot}/src/metadata/generated"],
|
||||
"options": {
|
||||
"cwd": "packages/twenty-client-sdk",
|
||||
"command": "tsx scripts/generate-metadata-client.ts"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/vitest:test",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"config": "{projectRoot}/vitest.config.ts"
|
||||
}
|
||||
},
|
||||
"set-local-version": {},
|
||||
"typecheck": {},
|
||||
"lint": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { getIntrospectionQuery, buildClientSchema, printSchema } from 'graphql';
|
||||
|
||||
import { generateMetadataClient } from '../src/generate/generate-metadata-client';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const TEMPLATE_PATH = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'src',
|
||||
'generate',
|
||||
'twenty-client-template.ts',
|
||||
);
|
||||
|
||||
const introspectSchema = async (url: string): Promise<string> => {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: getIntrospectionQuery() }),
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
if (json.errors) {
|
||||
throw new Error(
|
||||
`GraphQL introspection errors: ${JSON.stringify(json.errors)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return printSchema(buildClientSchema(json.data));
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const serverUrl = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
|
||||
|
||||
const schema = await introspectSchema(`${serverUrl}/metadata`);
|
||||
|
||||
const clientWrapperTemplateSource = await readFile(TEMPLATE_PATH, 'utf-8');
|
||||
|
||||
const outputPath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'src',
|
||||
'metadata',
|
||||
'generated',
|
||||
);
|
||||
|
||||
await generateMetadataClient({
|
||||
schema,
|
||||
outputPath,
|
||||
clientWrapperTemplateSource,
|
||||
});
|
||||
|
||||
console.log(`Metadata client generated at ${outputPath}`);
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Failed to generate metadata client:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
10
packages/twenty-client-sdk/src/core/generated/index.ts
Normal file
10
packages/twenty-client-sdk/src/core/generated/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// Stub — replaced at runtime by the generated client when the app
|
||||
// is installed on a Twenty instance or during `twenty app:dev`.
|
||||
export class CoreApiClient {
|
||||
constructor() {
|
||||
throw new Error(
|
||||
'CoreApiClient was not generated. ' +
|
||||
'Install this app on a Twenty instance or run `twenty app:dev`.',
|
||||
);
|
||||
}
|
||||
}
|
||||
2
packages/twenty-client-sdk/src/core/index.ts
Normal file
2
packages/twenty-client-sdk/src/core/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { CoreApiClient } from './generated/index';
|
||||
export * as CoreSchema from './generated/schema';
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import { transform } from 'esbuild';
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
import { transform } from 'esbuild';
|
||||
import {
|
||||
afterAll,
|
||||
beforeAll,
|
||||
|
|
@ -13,17 +15,12 @@ import {
|
|||
vi,
|
||||
} from 'vitest';
|
||||
|
||||
vi.mock('@/cli/constants/clients-dir', () => ({
|
||||
CLIENTS_GENERATED_DIR: 'src/clients/generated',
|
||||
}));
|
||||
import { buildClientWrapperSource } from '../client-wrapper';
|
||||
|
||||
vi.mock('twenty-shared/application', () => ({
|
||||
DEFAULT_APP_ACCESS_TOKEN_NAME: 'TWENTY_APP_ACCESS_TOKEN',
|
||||
DEFAULT_API_KEY_NAME: 'TWENTY_API_KEY',
|
||||
DEFAULT_API_URL_NAME: 'TWENTY_API_URL',
|
||||
}));
|
||||
|
||||
import { ClientService } from '@/cli/utilities/client/client-service';
|
||||
const twentyClientTemplateSource = readFileSync(
|
||||
join(__dirname, '..', 'twenty-client-template.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
type TwentyClassType = new (options?: {
|
||||
url?: string;
|
||||
|
|
@ -110,59 +107,33 @@ const getAuthorizationHeaderValue = (requestInit: RequestInit | undefined) => {
|
|||
return new Headers(requestInit?.headers).get('Authorization');
|
||||
};
|
||||
|
||||
describe('ClientService generated Twenty auth behavior', () => {
|
||||
let temporaryGeneratedClientDirectory: string;
|
||||
describe('Generated client wrapper auth behavior', () => {
|
||||
let temporaryDir: string;
|
||||
let TwentyClass: TwentyClassType;
|
||||
|
||||
beforeAll(async () => {
|
||||
temporaryGeneratedClientDirectory = await mkdtemp(
|
||||
join(tmpdir(), 'twenty-generated-client-'),
|
||||
);
|
||||
temporaryDir = await mkdtemp(join(tmpdir(), 'twenty-generated-client-'));
|
||||
|
||||
const temporaryGeneratedIndexTsPath = join(
|
||||
temporaryGeneratedClientDirectory,
|
||||
'index.ts',
|
||||
);
|
||||
|
||||
await writeFile(temporaryGeneratedIndexTsPath, stubGeneratedIndexSource);
|
||||
|
||||
const clientService = new ClientService();
|
||||
await (
|
||||
clientService as unknown as {
|
||||
injectClientWrapper: (
|
||||
output: string,
|
||||
options: {
|
||||
apiClientName: string;
|
||||
defaultUrl: string;
|
||||
includeUploadFile: boolean;
|
||||
},
|
||||
) => Promise<void>;
|
||||
}
|
||||
).injectClientWrapper(temporaryGeneratedClientDirectory, {
|
||||
const wrapperSource = buildClientWrapperSource(twentyClientTemplateSource, {
|
||||
apiClientName: 'MetadataApiClient',
|
||||
defaultUrl: '`${process.env.TWENTY_API_URL}/metadata`',
|
||||
includeUploadFile: true,
|
||||
});
|
||||
|
||||
const generatedIndexContent = await readFile(
|
||||
temporaryGeneratedIndexTsPath,
|
||||
'utf-8',
|
||||
);
|
||||
const fullSource = stubGeneratedIndexSource + wrapperSource;
|
||||
|
||||
const transpiledModule = await transform(generatedIndexContent, {
|
||||
const transpiledModule = await transform(fullSource, {
|
||||
loader: 'ts',
|
||||
format: 'esm',
|
||||
target: 'es2022',
|
||||
});
|
||||
|
||||
const temporaryGeneratedIndexMjsPath = join(
|
||||
temporaryGeneratedClientDirectory,
|
||||
'index.mjs',
|
||||
);
|
||||
await writeFile(temporaryGeneratedIndexMjsPath, transpiledModule.code);
|
||||
const outputPath = join(temporaryDir, 'index.mjs');
|
||||
|
||||
await writeFile(outputPath, transpiledModule.code);
|
||||
|
||||
const generatedModule = await import(
|
||||
`${pathToFileURL(temporaryGeneratedIndexMjsPath).href}?t=${Date.now()}`
|
||||
`${pathToFileURL(outputPath).href}?t=${Date.now()}`
|
||||
);
|
||||
|
||||
TwentyClass = generatedModule.MetadataApiClient as TwentyClassType;
|
||||
|
|
@ -176,11 +147,8 @@ describe('ClientService generated Twenty auth behavior', () => {
|
|||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (temporaryGeneratedClientDirectory) {
|
||||
await rm(temporaryGeneratedClientDirectory, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
if (temporaryDir) {
|
||||
await rm(temporaryDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -303,6 +271,7 @@ describe('ClientService generated Twenty auth behavior', () => {
|
|||
.fn<() => Promise<string>>()
|
||||
.mockImplementation(async () => {
|
||||
await Promise.resolve();
|
||||
|
||||
return 'fresh-token';
|
||||
});
|
||||
|
||||
51
packages/twenty-client-sdk/src/generate/client-wrapper.ts
Normal file
51
packages/twenty-client-sdk/src/generate/client-wrapper.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
type ClientWrapperOptions = {
|
||||
apiClientName: string;
|
||||
defaultUrl: string;
|
||||
includeUploadFile: boolean;
|
||||
};
|
||||
|
||||
const STRIPPED_TYPES_START = '// __STRIPPED_DURING_INJECTION_START__';
|
||||
const STRIPPED_TYPES_END = '// __STRIPPED_DURING_INJECTION_END__';
|
||||
const UPLOAD_FILE_START = '// __UPLOAD_FILE_START__';
|
||||
const UPLOAD_FILE_END = '// __UPLOAD_FILE_END__';
|
||||
|
||||
const escapeRegExp = (value: string): string =>
|
||||
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
export const buildClientWrapperSource = (
|
||||
templateSource: string,
|
||||
options: ClientWrapperOptions,
|
||||
): string => {
|
||||
let source = templateSource;
|
||||
|
||||
source = source.replace(
|
||||
new RegExp(
|
||||
`${escapeRegExp(STRIPPED_TYPES_START)}[\\s\\S]*?${escapeRegExp(STRIPPED_TYPES_END)}\\n?`,
|
||||
),
|
||||
'',
|
||||
);
|
||||
|
||||
source = source.replace("'__TWENTY_DEFAULT_URL__'", options.defaultUrl);
|
||||
|
||||
source = source.replace(/TwentyGeneratedClient/g, options.apiClientName);
|
||||
|
||||
if (!options.includeUploadFile) {
|
||||
source = source.replace(
|
||||
new RegExp(
|
||||
`\\s*${escapeRegExp(UPLOAD_FILE_START)}[\\s\\S]*?${escapeRegExp(UPLOAD_FILE_END)}\\n?`,
|
||||
),
|
||||
'\n',
|
||||
);
|
||||
} else {
|
||||
source = source.replace(
|
||||
new RegExp(`\\s*${escapeRegExp(UPLOAD_FILE_START)}\\n`),
|
||||
'\n',
|
||||
);
|
||||
source = source.replace(
|
||||
new RegExp(`\\s*${escapeRegExp(UPLOAD_FILE_END)}\\n`),
|
||||
'\n',
|
||||
);
|
||||
}
|
||||
|
||||
return `\n// ${options.apiClientName} (auto-injected by twenty-client-sdk)\n${source}`;
|
||||
};
|
||||
41
packages/twenty-client-sdk/src/generate/fs-utils.ts
Normal file
41
packages/twenty-client-sdk/src/generate/fs-utils.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { cp, mkdir, readdir, rename as fsRename, rm } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export const ensureDir = (dirPath: string) =>
|
||||
mkdir(dirPath, { recursive: true });
|
||||
|
||||
export const emptyDir = async (dirPath: string): Promise<void> => {
|
||||
let entries: string[];
|
||||
|
||||
try {
|
||||
entries = await readdir(dirPath);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
||||
await mkdir(dirPath, { recursive: true });
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
entries.map((entry) =>
|
||||
rm(join(dirPath, entry), { recursive: true, force: true }),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const move = async (src: string, dest: string): Promise<void> => {
|
||||
try {
|
||||
await fsRename(src, dest);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error && 'code' in error && error.code === 'EXDEV') {
|
||||
await cp(src, dest, { recursive: true });
|
||||
await rm(src, { recursive: true, force: true });
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const remove = (filePath: string) =>
|
||||
rm(filePath, { recursive: true, force: true });
|
||||
122
packages/twenty-client-sdk/src/generate/generate-core-client.ts
Normal file
122
packages/twenty-client-sdk/src/generate/generate-core-client.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import { appendFile, copyFile, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { generate } from '@genql/cli';
|
||||
import { build } from 'esbuild';
|
||||
import { DEFAULT_API_URL_NAME } from 'twenty-shared/application';
|
||||
|
||||
import { buildClientWrapperSource } from './client-wrapper';
|
||||
import { emptyDir, ensureDir, move, remove } from './fs-utils';
|
||||
import twentyClientTemplateSource from './twenty-client-template.ts?raw';
|
||||
|
||||
const COMMON_SCALAR_TYPES = {
|
||||
DateTime: 'string',
|
||||
JSON: 'Record<string, unknown>',
|
||||
UUID: 'string',
|
||||
};
|
||||
|
||||
export const GENERATED_CORE_DIR = 'core/generated';
|
||||
|
||||
// Generates the core API client from a GraphQL schema string.
|
||||
// Produces both TypeScript source and compiled ESM/CJS bundles.
|
||||
export const generateCoreClientFromSchema = async ({
|
||||
schema,
|
||||
outputPath,
|
||||
clientWrapperTemplateSource,
|
||||
}: {
|
||||
schema: string;
|
||||
outputPath: string;
|
||||
clientWrapperTemplateSource?: string;
|
||||
}): Promise<void> => {
|
||||
const templateSource =
|
||||
clientWrapperTemplateSource ?? twentyClientTemplateSource;
|
||||
const tempPath = `${outputPath}.tmp`;
|
||||
|
||||
await ensureDir(tempPath);
|
||||
await emptyDir(tempPath);
|
||||
|
||||
try {
|
||||
await generate({
|
||||
schema,
|
||||
output: tempPath,
|
||||
scalarTypes: COMMON_SCALAR_TYPES,
|
||||
});
|
||||
|
||||
const clientContent = buildClientWrapperSource(templateSource, {
|
||||
apiClientName: 'CoreApiClient',
|
||||
defaultUrl: `\`\${process.env.${DEFAULT_API_URL_NAME}}/graphql\``,
|
||||
includeUploadFile: true,
|
||||
});
|
||||
|
||||
await appendFile(join(tempPath, 'index.ts'), clientContent);
|
||||
|
||||
await remove(outputPath);
|
||||
await move(tempPath, outputPath);
|
||||
|
||||
await compileGeneratedClient(outputPath);
|
||||
} catch (error) {
|
||||
await remove(tempPath);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Generates the core client and replaces the pre-built stub inside
|
||||
// an installed twenty-client-sdk package (dist/core.mjs and dist/core.cjs).
|
||||
// Generated source files are kept in dist/generated-core/ for consumers
|
||||
// that need the raw .ts files (e.g. the app:dev upload step).
|
||||
export const replaceCoreClient = async ({
|
||||
packageRoot,
|
||||
schema,
|
||||
}: {
|
||||
packageRoot: string;
|
||||
schema: string;
|
||||
}): Promise<void> => {
|
||||
const generatedPath = join(packageRoot, 'dist', GENERATED_CORE_DIR);
|
||||
|
||||
await generateCoreClientFromSchema({ schema, outputPath: generatedPath });
|
||||
|
||||
await copyFile(
|
||||
join(generatedPath, 'index.mjs'),
|
||||
join(packageRoot, 'dist', 'core.mjs'),
|
||||
);
|
||||
await copyFile(
|
||||
join(generatedPath, 'index.cjs'),
|
||||
join(packageRoot, 'dist', 'core.cjs'),
|
||||
);
|
||||
};
|
||||
|
||||
const compileGeneratedClient = async (generatedDir: string): Promise<void> => {
|
||||
const entryPoint = join(generatedDir, 'index.ts');
|
||||
const outfile = join(generatedDir, 'index.mjs');
|
||||
|
||||
await build({
|
||||
entryPoints: [entryPoint],
|
||||
outfile,
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
sourcemap: false,
|
||||
minify: false,
|
||||
});
|
||||
|
||||
await build({
|
||||
entryPoints: [entryPoint],
|
||||
outfile: join(generatedDir, 'index.cjs'),
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
sourcemap: false,
|
||||
minify: false,
|
||||
});
|
||||
|
||||
await writeFile(
|
||||
join(generatedDir, 'package.json'),
|
||||
JSON.stringify(
|
||||
{ type: 'module', main: 'index.mjs', module: 'index.mjs' },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { appendFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { generate } from '@genql/cli';
|
||||
import { DEFAULT_API_URL_NAME } from 'twenty-shared/application';
|
||||
|
||||
import { buildClientWrapperSource } from './client-wrapper';
|
||||
import { emptyDir, ensureDir } from './fs-utils';
|
||||
import twentyClientTemplateSource from './twenty-client-template.ts?raw';
|
||||
|
||||
const COMMON_SCALAR_TYPES = {
|
||||
DateTime: 'string',
|
||||
JSON: 'Record<string, unknown>',
|
||||
UUID: 'string',
|
||||
};
|
||||
|
||||
export const generateMetadataClient = async ({
|
||||
schema,
|
||||
outputPath,
|
||||
clientWrapperTemplateSource,
|
||||
}: {
|
||||
schema: string;
|
||||
outputPath: string;
|
||||
clientWrapperTemplateSource?: string;
|
||||
}): Promise<void> => {
|
||||
const templateSource =
|
||||
clientWrapperTemplateSource ?? twentyClientTemplateSource;
|
||||
|
||||
await ensureDir(outputPath);
|
||||
await emptyDir(outputPath);
|
||||
|
||||
await generate({
|
||||
schema,
|
||||
output: outputPath,
|
||||
scalarTypes: {
|
||||
...COMMON_SCALAR_TYPES,
|
||||
Upload: 'File',
|
||||
},
|
||||
});
|
||||
|
||||
const clientContent = buildClientWrapperSource(templateSource, {
|
||||
apiClientName: 'MetadataApiClient',
|
||||
defaultUrl: `\`\${process.env.${DEFAULT_API_URL_NAME}}/metadata\``,
|
||||
includeUploadFile: true,
|
||||
});
|
||||
|
||||
await appendFile(join(outputPath, 'index.ts'), clientContent);
|
||||
};
|
||||
6
packages/twenty-client-sdk/src/generate/index.ts
Normal file
6
packages/twenty-client-sdk/src/generate/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export {
|
||||
GENERATED_CORE_DIR,
|
||||
generateCoreClientFromSchema,
|
||||
replaceCoreClient,
|
||||
} from './generate-core-client';
|
||||
export { generateMetadataClient } from './generate-metadata-client';
|
||||
4
packages/twenty-client-sdk/src/generate/raw-imports.d.ts
vendored
Normal file
4
packages/twenty-client-sdk/src/generate/raw-imports.d.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
declare module '*.ts?raw' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
|
@ -0,0 +1,434 @@
|
|||
// Ambient type stubs for the genql-generated code this template gets
|
||||
// injected into. They enable full typecheck/lint on this file.
|
||||
// __STRIPPED_DURING_INJECTION_START__
|
||||
type QueryGenqlSelection = Record<string, unknown>;
|
||||
type MutationGenqlSelection = Record<string, unknown>;
|
||||
type GraphqlOperation = Record<string, unknown>;
|
||||
|
||||
type ClientOptions = {
|
||||
url?: string;
|
||||
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
||||
fetcher?: (
|
||||
operation: GraphqlOperation | GraphqlOperation[],
|
||||
) => Promise<unknown>;
|
||||
fetch?: typeof globalThis.fetch;
|
||||
batch?: unknown;
|
||||
};
|
||||
|
||||
type Client = {
|
||||
query: (
|
||||
request: QueryGenqlSelection & { __name?: string },
|
||||
) => Promise<unknown>;
|
||||
mutation: (
|
||||
request: MutationGenqlSelection & { __name?: string },
|
||||
) => Promise<unknown>;
|
||||
};
|
||||
|
||||
declare function createClient(options: ClientOptions): Client;
|
||||
|
||||
declare class GenqlError extends Error {
|
||||
constructor(errors: unknown, data: unknown);
|
||||
}
|
||||
// __STRIPPED_DURING_INJECTION_END__
|
||||
|
||||
const APP_ACCESS_TOKEN_ENV_KEY = 'TWENTY_APP_ACCESS_TOKEN';
|
||||
const API_KEY_ENV_KEY = 'TWENTY_API_KEY';
|
||||
|
||||
type TwentyGeneratedClientOptions = ClientOptions;
|
||||
|
||||
type ProcessEnvironment = Record<string, string | undefined>;
|
||||
|
||||
type GraphqlErrorPayloadEntry = {
|
||||
message?: string;
|
||||
extensions?: { code?: string };
|
||||
};
|
||||
|
||||
type GraphqlResponsePayload = {
|
||||
data?: Record<string, unknown>;
|
||||
errors?: GraphqlErrorPayloadEntry[];
|
||||
};
|
||||
|
||||
type GraphqlResponse = {
|
||||
status: number;
|
||||
statusText: string;
|
||||
payload: GraphqlResponsePayload | null;
|
||||
rawBody: string;
|
||||
};
|
||||
|
||||
const getProcessEnvironment = (): ProcessEnvironment => {
|
||||
const processObject = (
|
||||
globalThis as { process?: { env?: ProcessEnvironment } }
|
||||
).process;
|
||||
|
||||
return processObject?.env ?? {};
|
||||
};
|
||||
|
||||
const getTokenFromAuthorizationHeader = (
|
||||
authorizationHeader: string | undefined,
|
||||
): string | null => {
|
||||
if (typeof authorizationHeader !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmedAuthorizationHeader = authorizationHeader.trim();
|
||||
|
||||
if (trimmedAuthorizationHeader.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (trimmedAuthorizationHeader === 'Bearer') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (trimmedAuthorizationHeader.startsWith('Bearer ')) {
|
||||
return trimmedAuthorizationHeader.slice('Bearer '.length).trim();
|
||||
}
|
||||
|
||||
return trimmedAuthorizationHeader;
|
||||
};
|
||||
|
||||
const getTokenFromHeaders = (
|
||||
headers: HeadersInit | undefined,
|
||||
): string | null => {
|
||||
if (!headers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (headers instanceof Headers) {
|
||||
return getTokenFromAuthorizationHeader(
|
||||
headers.get('Authorization') ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(headers)) {
|
||||
const matchedAuthorizationHeader = headers.find(
|
||||
([headerName]) => headerName.toLowerCase() === 'authorization',
|
||||
);
|
||||
|
||||
return getTokenFromAuthorizationHeader(matchedAuthorizationHeader?.[1]);
|
||||
}
|
||||
|
||||
const headersRecord = headers as Record<string, string | undefined>;
|
||||
|
||||
return getTokenFromAuthorizationHeader(
|
||||
headersRecord.Authorization ?? headersRecord.authorization,
|
||||
);
|
||||
};
|
||||
|
||||
const hasAuthenticationErrorInGraphqlPayload = (
|
||||
payload: GraphqlResponsePayload | null,
|
||||
): boolean => {
|
||||
if (!payload?.errors) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload.errors.some((graphqlError) => {
|
||||
return (
|
||||
graphqlError.extensions?.code === 'UNAUTHENTICATED' ||
|
||||
graphqlError.message?.toLowerCase() === 'unauthorized'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const defaultOptions: TwentyGeneratedClientOptions = {
|
||||
url: '__TWENTY_DEFAULT_URL__',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
export class TwentyGeneratedClient {
|
||||
private client: Client;
|
||||
private url: string;
|
||||
private requestOptions: RequestInit;
|
||||
private headers: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
||||
private fetchImplementation: typeof globalThis.fetch | null;
|
||||
private authorizationToken: string | null;
|
||||
private refreshAccessTokenPromise: Promise<string | null> | null = null;
|
||||
|
||||
constructor(options?: TwentyGeneratedClientOptions) {
|
||||
const merged: TwentyGeneratedClientOptions = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
};
|
||||
|
||||
const {
|
||||
url,
|
||||
headers,
|
||||
fetch: customFetchImplementation,
|
||||
fetcher: _fetcher,
|
||||
batch: _batch,
|
||||
...requestOptions
|
||||
} = merged;
|
||||
|
||||
this.url = url ?? '';
|
||||
this.requestOptions = requestOptions;
|
||||
this.headers = headers ?? {};
|
||||
this.fetchImplementation =
|
||||
customFetchImplementation ?? globalThis.fetch ?? null;
|
||||
|
||||
const processEnvironment = getProcessEnvironment();
|
||||
const tokenFromHeaders = getTokenFromHeaders(
|
||||
typeof headers === 'function' ? undefined : headers,
|
||||
);
|
||||
|
||||
// Priority: explicit header > app access token > api key (legacy).
|
||||
this.authorizationToken =
|
||||
tokenFromHeaders ??
|
||||
processEnvironment[APP_ACCESS_TOKEN_ENV_KEY] ??
|
||||
processEnvironment[API_KEY_ENV_KEY] ??
|
||||
null;
|
||||
|
||||
this.client = createClient({
|
||||
...merged,
|
||||
headers: undefined,
|
||||
fetcher: async (operation) =>
|
||||
this.executeGraphqlRequestWithOptionalRefresh({
|
||||
operation,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
query<R extends QueryGenqlSelection>(request: R & { __name?: string }) {
|
||||
return this.client.query(request);
|
||||
}
|
||||
|
||||
mutation<R extends MutationGenqlSelection>(request: R & { __name?: string }) {
|
||||
return this.client.mutation(request);
|
||||
}
|
||||
|
||||
// __UPLOAD_FILE_START__
|
||||
async uploadFile(
|
||||
fileBuffer: Buffer,
|
||||
filename: string,
|
||||
contentType: string = 'application/octet-stream',
|
||||
fieldMetadataUniversalIdentifier: string,
|
||||
): Promise<{
|
||||
id: string;
|
||||
path: string;
|
||||
size: number;
|
||||
createdAt: string;
|
||||
url: string;
|
||||
}> {
|
||||
const form = new FormData();
|
||||
|
||||
form.append(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
query: `mutation UploadFilesFieldFileByUniversalIdentifier($file: Upload!, $fieldMetadataUniversalIdentifier: String!) {
|
||||
uploadFilesFieldFileByUniversalIdentifier(file: $file, fieldMetadataUniversalIdentifier: $fieldMetadataUniversalIdentifier) { id path size createdAt url }
|
||||
}`,
|
||||
variables: {
|
||||
file: null,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
},
|
||||
}),
|
||||
);
|
||||
form.append('map', JSON.stringify({ '0': ['variables.file'] }));
|
||||
form.append(
|
||||
'0',
|
||||
new Blob([fileBuffer as BlobPart], { type: contentType }),
|
||||
filename,
|
||||
);
|
||||
|
||||
const result = await this.executeGraphqlRequestWithOptionalRefresh({
|
||||
operation: form,
|
||||
headers: {},
|
||||
requestInit: {
|
||||
method: 'POST',
|
||||
},
|
||||
});
|
||||
|
||||
if (result.errors) {
|
||||
throw new GenqlError(result.errors, result.data);
|
||||
}
|
||||
|
||||
const data = result.data as Record<string, unknown>;
|
||||
|
||||
return data.uploadFilesFieldFileByUniversalIdentifier as {
|
||||
id: string;
|
||||
path: string;
|
||||
size: number;
|
||||
createdAt: string;
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
// __UPLOAD_FILE_END__
|
||||
|
||||
private async executeGraphqlRequestWithOptionalRefresh({
|
||||
operation,
|
||||
headers,
|
||||
requestInit,
|
||||
}: {
|
||||
operation: GraphqlOperation | GraphqlOperation[] | FormData;
|
||||
headers?: HeadersInit;
|
||||
requestInit?: RequestInit;
|
||||
}) {
|
||||
const firstResponse = await this.executeGraphqlRequest({
|
||||
operation,
|
||||
headers,
|
||||
requestInit,
|
||||
token: this.authorizationToken,
|
||||
});
|
||||
|
||||
if (this.shouldRefreshToken(firstResponse)) {
|
||||
const refreshedAccessToken = await this.requestRefreshedAccessToken();
|
||||
|
||||
if (refreshedAccessToken) {
|
||||
const retryResponse = await this.executeGraphqlRequest({
|
||||
operation,
|
||||
headers,
|
||||
requestInit,
|
||||
token: refreshedAccessToken,
|
||||
});
|
||||
|
||||
return this.assertResponseIsSuccessful(retryResponse);
|
||||
}
|
||||
}
|
||||
|
||||
return this.assertResponseIsSuccessful(firstResponse);
|
||||
}
|
||||
|
||||
private async executeGraphqlRequest({
|
||||
operation,
|
||||
headers,
|
||||
requestInit,
|
||||
token,
|
||||
}: {
|
||||
operation: GraphqlOperation | GraphqlOperation[] | FormData;
|
||||
headers?: HeadersInit;
|
||||
requestInit?: RequestInit;
|
||||
token: string | null;
|
||||
}): Promise<GraphqlResponse> {
|
||||
if (!this.fetchImplementation) {
|
||||
throw new Error(
|
||||
'Global `fetch` function is not available, ' +
|
||||
'pass a fetch implementation to the Twenty client',
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedHeaders = await this.resolveHeaders();
|
||||
const requestHeaders = new Headers(resolvedHeaders);
|
||||
|
||||
if (headers) {
|
||||
new Headers(headers).forEach((value, key) =>
|
||||
requestHeaders.set(key, value),
|
||||
);
|
||||
}
|
||||
|
||||
if (operation instanceof FormData) {
|
||||
requestHeaders.delete('Content-Type');
|
||||
} else {
|
||||
requestHeaders.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
if (token) {
|
||||
requestHeaders.set('Authorization', `Bearer ${token}`);
|
||||
} else {
|
||||
requestHeaders.delete('Authorization');
|
||||
}
|
||||
|
||||
const response = await this.fetchImplementation.call(globalThis, this.url, {
|
||||
...this.requestOptions,
|
||||
...requestInit,
|
||||
method: requestInit?.method ?? 'POST',
|
||||
headers: requestHeaders,
|
||||
body:
|
||||
operation instanceof FormData ? operation : JSON.stringify(operation),
|
||||
});
|
||||
|
||||
const rawBody = await response.text();
|
||||
let payload: GraphqlResponsePayload | null = null;
|
||||
|
||||
if (rawBody.trim().length > 0) {
|
||||
try {
|
||||
payload = JSON.parse(rawBody) as GraphqlResponsePayload;
|
||||
} catch {
|
||||
payload = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
payload,
|
||||
rawBody,
|
||||
};
|
||||
}
|
||||
|
||||
private async resolveHeaders(): Promise<HeadersInit> {
|
||||
if (typeof this.headers === 'function') {
|
||||
return (await this.headers()) ?? {};
|
||||
}
|
||||
|
||||
return this.headers ?? {};
|
||||
}
|
||||
|
||||
private shouldRefreshToken(response: GraphqlResponse): boolean {
|
||||
if (response.status === 401) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return hasAuthenticationErrorInGraphqlPayload(response.payload);
|
||||
}
|
||||
|
||||
private assertResponseIsSuccessful(response: GraphqlResponse) {
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
throw new Error(`${response.statusText}: ${response.rawBody}`);
|
||||
}
|
||||
|
||||
if (response.payload === null) {
|
||||
throw new Error('Invalid JSON response');
|
||||
}
|
||||
|
||||
return response.payload;
|
||||
}
|
||||
|
||||
private async requestRefreshedAccessToken(): Promise<string | null> {
|
||||
const refreshAccessTokenFunction = (
|
||||
globalThis as {
|
||||
frontComponentHostCommunicationApi?: {
|
||||
requestAccessTokenRefresh?: () => Promise<string>;
|
||||
};
|
||||
}
|
||||
).frontComponentHostCommunicationApi?.requestAccessTokenRefresh;
|
||||
|
||||
if (typeof refreshAccessTokenFunction !== 'function') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.refreshAccessTokenPromise) {
|
||||
this.refreshAccessTokenPromise = refreshAccessTokenFunction()
|
||||
.then((refreshedAccessToken) => {
|
||||
if (
|
||||
typeof refreshedAccessToken !== 'string' ||
|
||||
refreshedAccessToken.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.setAuthorizationToken(refreshedAccessToken);
|
||||
|
||||
return refreshedAccessToken;
|
||||
})
|
||||
.catch((refreshError: unknown) => {
|
||||
console.error('Twenty client: token refresh failed', refreshError);
|
||||
|
||||
return null;
|
||||
})
|
||||
.finally(() => {
|
||||
this.refreshAccessTokenPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
return this.refreshAccessTokenPromise;
|
||||
}
|
||||
|
||||
private setAuthorizationToken(token: string) {
|
||||
this.authorizationToken = token;
|
||||
|
||||
const processEnvironment = getProcessEnvironment();
|
||||
|
||||
processEnvironment[APP_ACCESS_TOKEN_ENV_KEY] = token;
|
||||
}
|
||||
}
|
||||
|
|
@ -78,7 +78,7 @@ export const generateSubscriptionOp: (
|
|||
)
|
||||
}
|
||||
|
||||
// MetadataApiClient (auto-injected by twenty-sdk)
|
||||
// MetadataApiClient (auto-injected by twenty-client-sdk)
|
||||
// Ambient type stubs for the genql-generated code this template gets
|
||||
// injected into. They enable full typecheck/lint on this file.
|
||||
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
2
packages/twenty-client-sdk/src/metadata/index.ts
Normal file
2
packages/twenty-client-sdk/src/metadata/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { MetadataApiClient } from './generated/index';
|
||||
export * as MetadataSchema from './generated/schema';
|
||||
19
packages/twenty-client-sdk/tsconfig.json
Normal file
19
packages/twenty-client-sdk/tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strictNullChecks": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": false,
|
||||
"noEmit": true,
|
||||
"types": ["node"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "vite.config.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
17
packages/twenty-client-sdk/tsconfig.lib.json
Normal file
17
packages/twenty-client-sdk/tsconfig.lib.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": false,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
78
packages/twenty-client-sdk/vite.config.ts
Normal file
78
packages/twenty-client-sdk/vite.config.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import packageJson from './package.json';
|
||||
|
||||
const entries = [
|
||||
'src/core/index.ts',
|
||||
'src/metadata/index.ts',
|
||||
'src/generate/index.ts',
|
||||
];
|
||||
|
||||
const entryFileNames = (chunk: any, extension: 'cjs' | 'mjs') => {
|
||||
if (!chunk.isEntry) {
|
||||
throw new Error(
|
||||
`Should never occur, encountered a non entry chunk ${chunk.facadeModuleId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const splitFaceModuleId = chunk.facadeModuleId?.split('/');
|
||||
if (splitFaceModuleId === undefined) {
|
||||
throw new Error(
|
||||
`Should never occur, splitFaceModuleId is undefined ${chunk.facadeModuleId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const moduleDirectory = splitFaceModuleId[splitFaceModuleId?.length - 2];
|
||||
if (moduleDirectory === 'src') {
|
||||
return `${chunk.name}.${extension}`;
|
||||
}
|
||||
return `${moduleDirectory}.${extension}`;
|
||||
};
|
||||
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/packages/twenty-client-sdk',
|
||||
resolve: {
|
||||
alias: {
|
||||
'@/': path.resolve(__dirname, 'src') + '/',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
root: __dirname,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
emptyOutDir: false,
|
||||
outDir: 'dist',
|
||||
lib: { entry: entries, name: 'twenty-client-sdk' },
|
||||
rollupOptions: {
|
||||
external: [
|
||||
...Object.keys((packageJson as any).dependencies || {}),
|
||||
...Object.keys((packageJson as any).devDependencies || {}).filter(
|
||||
(dep: string) => dep !== 'twenty-shared',
|
||||
),
|
||||
'node:fs/promises',
|
||||
'node:fs',
|
||||
'node:path',
|
||||
],
|
||||
output: [
|
||||
{
|
||||
format: 'es',
|
||||
entryFileNames: (chunk) => entryFileNames(chunk, 'mjs'),
|
||||
},
|
||||
{
|
||||
format: 'cjs',
|
||||
interop: 'auto',
|
||||
esModule: true,
|
||||
exports: 'named',
|
||||
entryFileNames: (chunk) => entryFileNames(chunk, 'cjs'),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
logLevel: 'warn',
|
||||
};
|
||||
});
|
||||
14
packages/twenty-client-sdk/vitest.config.ts
Normal file
14
packages/twenty-client-sdk/vitest.config.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
name: 'twenty-client-sdk',
|
||||
environment: 'node',
|
||||
include: [
|
||||
'src/**/__tests__/**/*.{test,spec}.{ts,tsx}',
|
||||
'src/**/*.{test,spec}.{ts,tsx}',
|
||||
],
|
||||
exclude: ['**/node_modules/**', '**/.git/**'],
|
||||
globals: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -546,7 +546,7 @@ Each function file uses `defineLogicFunction()` to export a configuration with a
|
|||
// src/app/createPostCard.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
|
||||
import { CoreApiClient, type Person } from 'twenty-sdk/clients';
|
||||
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: RoutePayload) => {
|
||||
const client = new CoreApiClient();
|
||||
|
|
@ -784,7 +784,7 @@ To mark a logic function as a tool, set `isTool: true` and provide a `toolInputS
|
|||
```typescript
|
||||
// src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
const client = new CoreApiClient();
|
||||
|
|
@ -1200,8 +1200,8 @@ Two typed clients are auto-generated by `yarn twenty dev` and stored in `node_mo
|
|||
- **`MetadataApiClient`** — queries the `/metadata` endpoint for workspace configuration and file uploads
|
||||
|
||||
```typescript
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
import { MetadataApiClient } from 'twenty-sdk/clients';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
|
||||
const client = new CoreApiClient();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
|
|
@ -1229,7 +1229,7 @@ Notes:
|
|||
The `MetadataApiClient` includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Because standard GraphQL clients do not support multipart file uploads natively, the client provides this dedicated method that implements the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) under the hood.
|
||||
|
||||
```typescript
|
||||
import { MetadataApiClient } from 'twenty-sdk/clients';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const metadataClient = new MetadataApiClient();
|
||||
|
|
|
|||
|
|
@ -1873,6 +1873,7 @@ export enum FileFolder {
|
|||
Dependencies = 'Dependencies',
|
||||
File = 'File',
|
||||
FilesField = 'FilesField',
|
||||
GeneratedSdkClient = 'GeneratedSdkClient',
|
||||
PersonPicture = 'PersonPicture',
|
||||
ProfilePicture = 'ProfilePicture',
|
||||
PublicAsset = 'PublicAsset',
|
||||
|
|
@ -1920,6 +1921,7 @@ export type FrontComponent = {
|
|||
sourceComponentPath: Scalars['String'];
|
||||
universalIdentifier?: Maybe<Scalars['UUID']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
usesSdkClient: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type FrontComponentConfiguration = {
|
||||
|
|
@ -6640,7 +6642,7 @@ export type FindOneFrontComponentQueryVariables = Exact<{
|
|||
}>;
|
||||
|
||||
|
||||
export type FindOneFrontComponentQuery = { __typename?: 'Query', frontComponent?: { __typename?: 'FrontComponent', id: string, name: string, applicationId: string, builtComponentChecksum: string, isHeadless: boolean, applicationTokenPair?: { __typename?: 'ApplicationTokenPair', applicationAccessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, applicationRefreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } | null } | null };
|
||||
export type FindOneFrontComponentQuery = { __typename?: 'Query', frontComponent?: { __typename?: 'FrontComponent', id: string, name: string, applicationId: string, builtComponentChecksum: string, isHeadless: boolean, usesSdkClient: boolean, applicationTokenPair?: { __typename?: 'ApplicationTokenPair', applicationAccessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, applicationRefreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } | null } | null };
|
||||
|
||||
export type LogicFunctionFieldsFragment = { __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, cronTriggerSettings?: any | null, databaseEventTriggerSettings?: any | null, httpRouteTriggerSettings?: any | null, applicationId?: string | null, createdAt: string, updatedAt: string };
|
||||
|
||||
|
|
@ -8170,7 +8172,7 @@ export const UploadFilesFieldFileDocument = {"kind":"Document","definitions":[{"
|
|||
export const UploadWorkflowFileDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UploadWorkflowFile"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"file"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Upload"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uploadWorkflowFile"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"file"},"value":{"kind":"Variable","name":{"kind":"Name","value":"file"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode<UploadWorkflowFileMutation, UploadWorkflowFileMutationVariables>;
|
||||
export const RenewApplicationTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RenewApplicationToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRefreshToken"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"renewApplicationToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRefreshToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRefreshToken"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"applicationAccessToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"applicationRefreshToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]}}]} as unknown as DocumentNode<RenewApplicationTokenMutation, RenewApplicationTokenMutationVariables>;
|
||||
export const FindManyFrontComponentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyFrontComponents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"frontComponents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}}]}}]}}]} as unknown as DocumentNode<FindManyFrontComponentsQuery, FindManyFrontComponentsQueryVariables>;
|
||||
export const FindOneFrontComponentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneFrontComponent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"frontComponent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"builtComponentChecksum"}},{"kind":"Field","name":{"kind":"Name","value":"isHeadless"}},{"kind":"Field","name":{"kind":"Name","value":"applicationTokenPair"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"applicationAccessToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"applicationRefreshToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]}}]}}]} as unknown as DocumentNode<FindOneFrontComponentQuery, FindOneFrontComponentQueryVariables>;
|
||||
export const FindOneFrontComponentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneFrontComponent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"frontComponent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"builtComponentChecksum"}},{"kind":"Field","name":{"kind":"Name","value":"isHeadless"}},{"kind":"Field","name":{"kind":"Name","value":"usesSdkClient"}},{"kind":"Field","name":{"kind":"Name","value":"applicationTokenPair"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"applicationAccessToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"applicationRefreshToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]}}]}}]} as unknown as DocumentNode<FindOneFrontComponentQuery, FindOneFrontComponentQueryVariables>;
|
||||
export const CreateOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateLogicFunctionFromSourceInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LogicFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LogicFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"sourceHandlerPath"}},{"kind":"Field","name":{"kind":"Name","value":"handlerName"}},{"kind":"Field","name":{"kind":"Name","value":"toolInputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"isTool"}},{"kind":"Field","name":{"kind":"Name","value":"cronTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"databaseEventTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"httpRouteTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<CreateOneLogicFunctionMutation, CreateOneLogicFunctionMutationVariables>;
|
||||
export const DeleteOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LogicFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LogicFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"sourceHandlerPath"}},{"kind":"Field","name":{"kind":"Name","value":"handlerName"}},{"kind":"Field","name":{"kind":"Name","value":"toolInputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"isTool"}},{"kind":"Field","name":{"kind":"Name","value":"cronTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"databaseEventTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"httpRouteTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<DeleteOneLogicFunctionMutation, DeleteOneLogicFunctionMutationVariables>;
|
||||
export const ExecuteOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ExecuteOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ExecuteOneLogicFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"executeOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"logs"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<ExecuteOneLogicFunctionMutation, ExecuteOneLogicFunctionMutationVariables>;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { FrontComponentRendererProvider } from '@/front-components/components/FrontComponentRendererProvider';
|
||||
import { FrontComponentRendererWithSdkClient } from '@/front-components/components/FrontComponentRendererWithSdkClient';
|
||||
import { useFrontComponentExecutionContext } from '@/front-components/hooks/useFrontComponentExecutionContext';
|
||||
import { useOnFrontComponentUpdated } from '@/front-components/hooks/useOnFrontComponentUpdated';
|
||||
import { frontComponentApplicationTokenPairComponentState } from '@/front-components/states/frontComponentApplicationTokenPairComponentState';
|
||||
|
|
@ -73,11 +74,6 @@ export const FrontComponentRenderer = ({
|
|||
frontComponentId,
|
||||
});
|
||||
|
||||
const componentUrl = getFrontComponentUrl({
|
||||
frontComponentId,
|
||||
checksum: data?.frontComponent?.builtComponentChecksum,
|
||||
});
|
||||
|
||||
const applicationTokenPair =
|
||||
data?.frontComponent?.applicationTokenPair ?? null;
|
||||
|
||||
|
|
@ -89,14 +85,39 @@ export const FrontComponentRenderer = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
const componentUrl = getFrontComponentUrl({
|
||||
frontComponentId,
|
||||
checksum: data.frontComponent.builtComponentChecksum,
|
||||
});
|
||||
|
||||
const usesSdkClient = data.frontComponent.usesSdkClient;
|
||||
|
||||
const accessToken = applicationTokenPair.applicationAccessToken.token;
|
||||
|
||||
if (usesSdkClient) {
|
||||
return (
|
||||
<FrontComponentRendererProvider frontComponentId={frontComponentId}>
|
||||
<FrontComponentRendererWithSdkClient
|
||||
colorScheme={colorScheme}
|
||||
componentUrl={componentUrl}
|
||||
applicationAccessToken={accessToken}
|
||||
applicationId={data.frontComponent.applicationId}
|
||||
executionContext={executionContext}
|
||||
frontComponentHostCommunicationApi={
|
||||
frontComponentHostCommunicationApi
|
||||
}
|
||||
onError={handleError}
|
||||
/>
|
||||
</FrontComponentRendererProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FrontComponentRendererProvider frontComponentId={frontComponentId}>
|
||||
<SharedFrontComponentRenderer
|
||||
colorScheme={colorScheme}
|
||||
componentUrl={componentUrl}
|
||||
applicationAccessToken={
|
||||
applicationTokenPair.applicationAccessToken.token
|
||||
}
|
||||
applicationAccessToken={accessToken}
|
||||
apiUrl={REACT_APP_SERVER_BASE_URL}
|
||||
executionContext={executionContext}
|
||||
frontComponentHostCommunicationApi={frontComponentHostCommunicationApi}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
import { useAtomValue } from 'jotai';
|
||||
|
||||
import { SdkClientBlobUrlsEffect } from '@/front-components/components/SdkClientBlobUrlsEffect';
|
||||
import { sdkClientFamilyState } from '@/front-components/states/sdkClientFamilyState';
|
||||
import {
|
||||
FrontComponentRenderer as SharedFrontComponentRenderer,
|
||||
type FrontComponentExecutionContext,
|
||||
type FrontComponentHostCommunicationApi,
|
||||
} from 'twenty-sdk/front-component-renderer';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
|
||||
type FrontComponentRendererWithSdkClientProps = {
|
||||
colorScheme: 'light' | 'dark';
|
||||
componentUrl: string;
|
||||
applicationAccessToken: string;
|
||||
applicationId: string;
|
||||
executionContext: FrontComponentExecutionContext;
|
||||
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
|
||||
onError: (error?: Error) => void;
|
||||
};
|
||||
|
||||
export const FrontComponentRendererWithSdkClient = ({
|
||||
colorScheme,
|
||||
componentUrl,
|
||||
applicationAccessToken,
|
||||
applicationId,
|
||||
executionContext,
|
||||
frontComponentHostCommunicationApi,
|
||||
onError,
|
||||
}: FrontComponentRendererWithSdkClientProps) => {
|
||||
const sdkClientState = useAtomValue(
|
||||
sdkClientFamilyState.atomFamily(applicationId),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SdkClientBlobUrlsEffect
|
||||
applicationId={applicationId}
|
||||
accessToken={applicationAccessToken}
|
||||
onError={onError}
|
||||
/>
|
||||
{sdkClientState.status === 'loaded' && (
|
||||
<SharedFrontComponentRenderer
|
||||
colorScheme={colorScheme}
|
||||
componentUrl={componentUrl}
|
||||
applicationAccessToken={applicationAccessToken}
|
||||
apiUrl={REACT_APP_SERVER_BASE_URL}
|
||||
sdkClientUrls={sdkClientState.blobUrls}
|
||||
executionContext={executionContext}
|
||||
frontComponentHostCommunicationApi={
|
||||
frontComponentHostCommunicationApi
|
||||
}
|
||||
onError={onError}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { useStore } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { sdkClientFamilyState } from '@/front-components/states/sdkClientFamilyState';
|
||||
import { fetchSdkClientBlobUrls } from '@/front-components/utils/fetchSdkClientBlobUrls';
|
||||
|
||||
export const SdkClientBlobUrlsEffect = ({
|
||||
applicationId,
|
||||
accessToken,
|
||||
onError,
|
||||
}: {
|
||||
applicationId: string;
|
||||
accessToken: string;
|
||||
onError?: (error: Error) => void;
|
||||
}) => {
|
||||
const store = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
const atom = sdkClientFamilyState.atomFamily(applicationId);
|
||||
const { status } = store.get(atom);
|
||||
|
||||
if (status === 'loading' || status === 'loaded') {
|
||||
return;
|
||||
}
|
||||
|
||||
store.set(atom, { status: 'loading' });
|
||||
|
||||
const fetchBlobUrls = async () => {
|
||||
try {
|
||||
const blobUrls = await fetchSdkClientBlobUrls(
|
||||
applicationId,
|
||||
accessToken,
|
||||
);
|
||||
|
||||
store.set(atom, { status: 'loaded', blobUrls });
|
||||
} catch (error: unknown) {
|
||||
const normalizedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
store.set(atom, { status: 'error', error: normalizedError });
|
||||
onError?.(normalizedError);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBlobUrls();
|
||||
}, [applicationId, accessToken, store, onError]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -8,6 +8,7 @@ export const FIND_ONE_FRONT_COMPONENT = gql`
|
|||
applicationId
|
||||
builtComponentChecksum
|
||||
isHeadless
|
||||
usesSdkClient
|
||||
applicationTokenPair {
|
||||
applicationAccessToken {
|
||||
token
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { createAtomFamilyState } from '@/ui/utilities/state/jotai/utils/createAtomFamilyState';
|
||||
|
||||
export type SdkClientBlobUrls = {
|
||||
core: string;
|
||||
metadata: string;
|
||||
};
|
||||
|
||||
export type SdkClientState =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'loaded'; blobUrls: SdkClientBlobUrls }
|
||||
| { status: 'error'; error: Error };
|
||||
|
||||
export const sdkClientFamilyState = createAtomFamilyState<
|
||||
SdkClientState,
|
||||
string
|
||||
>({
|
||||
key: 'sdkClientFamilyState',
|
||||
defaultValue: { status: 'idle' },
|
||||
});
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { type SdkClientBlobUrls } from '@/front-components/states/sdkClientFamilyState';
|
||||
import { getSdkClientUrls } from '@/front-components/utils/getSdkClientUrls';
|
||||
|
||||
const fetchAndCreateBlobUrl = async (
|
||||
url: string,
|
||||
token: string,
|
||||
): Promise<string> => {
|
||||
const response = await fetch(url, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch SDK module from ${url}: ${response.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
const source = await response.text();
|
||||
const blob = new Blob([source], { type: 'application/javascript' });
|
||||
|
||||
return URL.createObjectURL(blob);
|
||||
};
|
||||
|
||||
export const fetchSdkClientBlobUrls = async (
|
||||
applicationId: string,
|
||||
accessToken: string,
|
||||
): Promise<SdkClientBlobUrls> => {
|
||||
const urls = getSdkClientUrls(applicationId);
|
||||
|
||||
const [coreResult, metadataResult] = await Promise.allSettled([
|
||||
fetchAndCreateBlobUrl(urls.core, accessToken),
|
||||
fetchAndCreateBlobUrl(urls.metadata, accessToken),
|
||||
]);
|
||||
|
||||
if (
|
||||
coreResult.status === 'fulfilled' &&
|
||||
metadataResult.status === 'fulfilled'
|
||||
) {
|
||||
return { core: coreResult.value, metadata: metadataResult.value };
|
||||
}
|
||||
|
||||
if (coreResult.status === 'fulfilled') {
|
||||
URL.revokeObjectURL(coreResult.value);
|
||||
}
|
||||
|
||||
if (metadataResult.status === 'fulfilled') {
|
||||
URL.revokeObjectURL(metadataResult.value);
|
||||
}
|
||||
|
||||
throw coreResult.status === 'rejected'
|
||||
? coreResult.reason
|
||||
: metadataResult.status === 'rejected'
|
||||
? metadataResult.reason
|
||||
: new Error('Unexpected SDK client fetch failure');
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
|
||||
|
||||
export const getSdkClientUrls = (applicationId: string) => ({
|
||||
core: `${REST_API_BASE_URL}/sdk-client/${applicationId}/core`,
|
||||
metadata: `${REST_API_BASE_URL}/sdk-client/${applicationId}/metadata`,
|
||||
});
|
||||
|
|
@ -56,7 +56,7 @@ export const useLoadCurrentUser = () => {
|
|||
|
||||
const user = currentUserResult.data?.currentUser;
|
||||
|
||||
if (!user) {
|
||||
if (!isDefined(user)) {
|
||||
throw new Error('No current user result');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist", "src/clients/generated"],
|
||||
"ignorePatterns": ["node_modules", "dist"],
|
||||
"rules": {
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
"no-console": "off",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
dist
|
||||
storybook-static
|
||||
coverage
|
||||
src/clients/generated
|
||||
|
|
|
|||
|
|
@ -48,11 +48,6 @@
|
|||
"types": "./dist/build/index.d.ts",
|
||||
"import": "./dist/build.mjs",
|
||||
"require": "./dist/build.cjs"
|
||||
},
|
||||
"./clients": {
|
||||
"types": "./dist/clients/index.d.ts",
|
||||
"import": "./dist/clients.mjs",
|
||||
"require": "./dist/clients.cjs"
|
||||
}
|
||||
},
|
||||
"license": "AGPL-3.0",
|
||||
|
|
@ -101,6 +96,7 @@
|
|||
"storybook": "^10.2.13",
|
||||
"ts-morph": "^25.0.0",
|
||||
"tsx": "^4.7.0",
|
||||
"twenty-client-sdk": "workspace:*",
|
||||
"twenty-shared": "workspace:*",
|
||||
"twenty-ui": "workspace:*",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
|
|
@ -120,9 +116,6 @@
|
|||
],
|
||||
"front-component-renderer": [
|
||||
"dist/front-component-renderer/index.d.ts"
|
||||
],
|
||||
"clients": [
|
||||
"dist/clients/index.d.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,16 +96,6 @@
|
|||
"command": "npx vite build -c vite.config.sdk.ts"
|
||||
}
|
||||
},
|
||||
"generate-metadata-client": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": false,
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["{projectRoot}/src/clients/generated/metadata"],
|
||||
"options": {
|
||||
"cwd": "packages/twenty-sdk",
|
||||
"command": "tsx -r tsconfig-paths/register scripts/generate-metadata-client.ts"
|
||||
}
|
||||
},
|
||||
"generate-remote-dom-elements": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import { CLIENTS_GENERATED_DIR } from '@/cli/constants/clients-dir';
|
||||
import { ClientService } from '@/cli/utilities/client/client-service';
|
||||
|
||||
const TEMPLATE_PATH = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'src',
|
||||
'cli',
|
||||
'utilities',
|
||||
'client',
|
||||
'twenty-client-template.ts',
|
||||
);
|
||||
|
||||
const main = async () => {
|
||||
const outputPath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
CLIENTS_GENERATED_DIR,
|
||||
'metadata',
|
||||
);
|
||||
|
||||
const serverUrl = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
|
||||
const clientWrapperTemplateSource = await readFile(TEMPLATE_PATH, 'utf-8');
|
||||
|
||||
const clientService = new ClientService({
|
||||
clientWrapperTemplateSource,
|
||||
serverUrl,
|
||||
skipAuth: true,
|
||||
});
|
||||
|
||||
await clientService.generateMetadataClient({ outputPath });
|
||||
|
||||
console.log(`Metadata client generated at ${outputPath}`);
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Failed to generate metadata client:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { vi } from 'vitest';
|
||||
|
||||
import { appBuild } from '@/cli/operations/build';
|
||||
import { appDeploy } from '@/cli/operations/deploy';
|
||||
import { appInstall } from '@/cli/operations/install';
|
||||
import { appUninstall } from '@/cli/operations/uninstall';
|
||||
import { functionExecute } from '@/cli/operations/execute';
|
||||
import { FUNCTION_EXECUTE_APP_PATH } from '@/cli/__tests__/apps/fixture-paths';
|
||||
|
|
@ -11,7 +13,7 @@ const APP_PATH = FUNCTION_EXECUTE_APP_PATH;
|
|||
|
||||
describe('functionExecute E2E', () => {
|
||||
beforeAll(async () => {
|
||||
const buildResult = await appBuild({ appPath: APP_PATH });
|
||||
const buildResult = await appBuild({ appPath: APP_PATH, tarball: true });
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(
|
||||
|
|
@ -19,6 +21,24 @@ describe('functionExecute E2E', () => {
|
|||
);
|
||||
}
|
||||
|
||||
const deployResult = await appDeploy({
|
||||
tarballPath: buildResult.data.tarballPath!,
|
||||
});
|
||||
|
||||
if (!deployResult.success) {
|
||||
throw new Error(
|
||||
`appDeploy failed: ${deployResult.error.code} – ${deployResult.error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
const installResult = await appInstall({ appPath: APP_PATH });
|
||||
|
||||
if (!installResult.success) {
|
||||
throw new Error(
|
||||
`appInstall failed: ${installResult.error.code} – ${installResult.error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
await vi.waitFor(
|
||||
async () => {
|
||||
const result = await functionExecute({
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
|||
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
|
||||
packageJsonChecksum: '[checksum]',
|
||||
yarnLockChecksum: '[checksum]',
|
||||
apiClientChecksum: null,
|
||||
},
|
||||
skills: [],
|
||||
agents: [],
|
||||
|
|
@ -345,6 +344,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
|||
builtComponentPath: 'my.front-component.mjs',
|
||||
builtComponentChecksum: '[checksum]',
|
||||
isHeadless: false,
|
||||
usesSdkClient: false,
|
||||
},
|
||||
],
|
||||
views: [],
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
|||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e',
|
||||
packageJsonChecksum: '2851d0e2c3621a57e1fd103a245b6fde',
|
||||
apiClientChecksum: null,
|
||||
},
|
||||
frontComponents: [
|
||||
{
|
||||
|
|
@ -61,6 +60,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
|||
sourceComponentPath: 'src/root.front-component.tsx',
|
||||
universalIdentifier: 'a0a1a2a3-a4a5-4000-8000-000000000001',
|
||||
isHeadless: false,
|
||||
usesSdkClient: false,
|
||||
},
|
||||
{
|
||||
builtComponentPath: 'src/components/card.front-component.mjs',
|
||||
|
|
@ -71,6 +71,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
|||
sourceComponentPath: 'src/components/card.front-component.tsx',
|
||||
universalIdentifier: '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7',
|
||||
isHeadless: false,
|
||||
usesSdkClient: false,
|
||||
},
|
||||
{
|
||||
builtComponentPath: 'src/components/greeting.front-component.mjs',
|
||||
|
|
@ -81,6 +82,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
|||
sourceComponentPath: 'src/components/greeting.front-component.tsx',
|
||||
universalIdentifier: '370ae182-743f-4ecb-b625-7ac48e21f0e5',
|
||||
isHeadless: false,
|
||||
usesSdkClient: false,
|
||||
},
|
||||
{
|
||||
builtComponentPath: 'src/components/test.front-component.mjs',
|
||||
|
|
@ -91,6 +93,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
|||
sourceComponentPath: 'src/components/test.front-component.tsx',
|
||||
universalIdentifier: 'f1234567-abcd-4000-8000-000000000001',
|
||||
isHeadless: false,
|
||||
usesSdkClient: false,
|
||||
},
|
||||
],
|
||||
|
||||
|
|
@ -269,7 +272,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
|||
},
|
||||
],
|
||||
type: FieldType.SELECT,
|
||||
universalIdentifier: '8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e',
|
||||
universalIdentifier: 'b602dbd9-e511-49ce-b6d3-b697218dc69c',
|
||||
},
|
||||
{
|
||||
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
|
|
@ -277,7 +280,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
|||
label: 'Priority',
|
||||
name: 'priority',
|
||||
type: FieldType.NUMBER,
|
||||
universalIdentifier: '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d',
|
||||
universalIdentifier: '7b57bd63-5a4c-46ca-9d52-42c8f02d1df6',
|
||||
},
|
||||
{
|
||||
label: 'Recipient',
|
||||
|
|
@ -1266,8 +1269,8 @@ export const EXPECTED_MANIFEST: Manifest = {
|
|||
},
|
||||
{
|
||||
icon: 'IconHome',
|
||||
label: 'Address',
|
||||
name: 'address',
|
||||
label: 'Mailing Address',
|
||||
name: 'mailingAddress',
|
||||
type: FieldType.ADDRESS,
|
||||
universalIdentifier: 'd3a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,9 +17,6 @@ export const normalizeManifestForComparison = <T extends Manifest>(
|
|||
packageJsonChecksum: manifest.application.packageJsonChecksum
|
||||
? '[checksum]'
|
||||
: null,
|
||||
apiClientChecksum: manifest.application.apiClientChecksum
|
||||
? '[checksum]'
|
||||
: null,
|
||||
},
|
||||
objects: sortById(
|
||||
manifest.objects.map((object) => ({
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import chalk from 'chalk';
|
|||
import type { Command } from 'commander';
|
||||
import { AppBuildCommand } from './build';
|
||||
import { AppDevCommand } from './dev';
|
||||
import { AppInstallCommand } from './install';
|
||||
import { AppPublishCommand } from './publish';
|
||||
import { AppTypecheckCommand } from './typecheck';
|
||||
import { AppUninstallCommand } from './uninstall';
|
||||
|
|
@ -17,6 +18,7 @@ import { SyncableEntity } from 'twenty-shared/application';
|
|||
export const registerCommands = (program: Command): void => {
|
||||
const buildCommand = new AppBuildCommand();
|
||||
const devCommand = new AppDevCommand();
|
||||
const installCommand = new AppInstallCommand();
|
||||
const publishCommand = new AppPublishCommand();
|
||||
const typecheckCommand = new AppTypecheckCommand();
|
||||
const uninstallCommand = new AppUninstallCommand();
|
||||
|
|
@ -45,9 +47,20 @@ export const registerCommands = (program: Command): void => {
|
|||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('install [appPath]')
|
||||
.description('Install a deployed application on the connected server')
|
||||
.option('-r, --remote <name>', 'Install on a specific remote')
|
||||
.action(async (appPath, options) => {
|
||||
await installCommand.execute({
|
||||
appPath: formatPath(appPath),
|
||||
remote: options.remote,
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('deploy [appPath]')
|
||||
.description('Build and deploy to a Twenty server')
|
||||
.description('Build and upload application to a Twenty server')
|
||||
.option('-r, --remote <name>', 'Deploy to a specific remote')
|
||||
.action(async (appPath, options) => {
|
||||
await deployCommand.execute({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { appBuild } from '@/cli/operations/build';
|
||||
import { appDeploy } from '@/cli/operations/deploy';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import { checkSdkVersionCompatibility } from '@/cli/utilities/version/check-sdk-version-compatibility';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export type DeployCommandOptions = {
|
||||
|
|
@ -15,34 +15,26 @@ export class DeployCommand {
|
|||
|
||||
await checkSdkVersionCompatibility(appPath);
|
||||
|
||||
const configService = new ConfigService();
|
||||
let serverUrl: string;
|
||||
let token: string | undefined;
|
||||
|
||||
if (options.remote) {
|
||||
const remoteConfig = await configService.getConfigForRemote(
|
||||
options.remote,
|
||||
);
|
||||
|
||||
serverUrl = remoteConfig.apiUrl;
|
||||
token = remoteConfig.accessToken ?? remoteConfig.apiKey;
|
||||
} else {
|
||||
const config = await configService.getConfig();
|
||||
|
||||
serverUrl = config.apiUrl;
|
||||
token = config.accessToken ?? config.apiKey;
|
||||
}
|
||||
|
||||
const remoteName = options.remote ?? ConfigService.getActiveRemote();
|
||||
|
||||
console.log(chalk.blue(`Deploying to ${remoteName} (${serverUrl})...`));
|
||||
console.log(chalk.blue('Deploying application...'));
|
||||
console.log(chalk.gray(`App path: ${appPath}\n`));
|
||||
|
||||
const result = await appDeploy({
|
||||
const onProgress = (message: string) => console.log(chalk.gray(message));
|
||||
|
||||
const buildResult = await appBuild({
|
||||
appPath,
|
||||
serverUrl,
|
||||
token,
|
||||
onProgress: (message) => console.log(chalk.gray(message)),
|
||||
tarball: true,
|
||||
onProgress,
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
console.error(chalk.red(buildResult.error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const result = await appDeploy({
|
||||
tarballPath: buildResult.data.tarballPath!,
|
||||
remote: options.remote,
|
||||
onProgress,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
|
|
|
|||
32
packages/twenty-sdk/src/cli/commands/install.ts
Normal file
32
packages/twenty-sdk/src/cli/commands/install.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { appInstall } from '@/cli/operations/install';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import { checkSdkVersionCompatibility } from '@/cli/utilities/version/check-sdk-version-compatibility';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export type AppInstallCommandOptions = {
|
||||
appPath?: string;
|
||||
remote?: string;
|
||||
};
|
||||
|
||||
export class AppInstallCommand {
|
||||
async execute(options: AppInstallCommandOptions): Promise<void> {
|
||||
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
await checkSdkVersionCompatibility(appPath);
|
||||
|
||||
console.log(chalk.blue('Installing application...'));
|
||||
console.log(chalk.gray(`App path: ${appPath}\n`));
|
||||
|
||||
const result = await appInstall({
|
||||
appPath,
|
||||
remote: options.remote,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error(chalk.red(result.error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.green('✓ Application installed'));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export const CLIENTS_SOURCE_DIR = 'src/clients';
|
||||
export const CLIENTS_GENERATED_DIR = `${CLIENTS_SOURCE_DIR}/generated`;
|
||||
|
|
@ -2,10 +2,10 @@ import { execSync } from 'child_process';
|
|||
import path from 'path';
|
||||
|
||||
import { buildApplication } from '@/cli/utilities/build/common/build-application';
|
||||
import { synchronizeBuiltApplication } from '@/cli/utilities/build/common/synchronize-built-application';
|
||||
import { runTypecheck } from '@/cli/utilities/build/common/typecheck-plugin';
|
||||
import { buildAndValidateManifest } from '@/cli/utilities/build/manifest/build-and-validate-manifest';
|
||||
import { ClientService } from '@/cli/utilities/client/client-service';
|
||||
import { manifestUpdateChecksums } from '@/cli/utilities/build/manifest/manifest-update-checksums';
|
||||
import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer';
|
||||
import { runSafe } from '@/cli/utilities/run-safe';
|
||||
import { APP_ERROR_CODES, type CommandResult } from '@/cli/types';
|
||||
|
||||
|
|
@ -48,30 +48,12 @@ const innerAppBuild = async (
|
|||
|
||||
onProgress?.('Building application files...');
|
||||
|
||||
const firstBuildResult = await buildApplication({
|
||||
const buildResult = await buildApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
filePaths,
|
||||
});
|
||||
|
||||
onProgress?.('Syncing application schema...');
|
||||
|
||||
const firstSyncResult = await synchronizeBuiltApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
builtFileInfos: firstBuildResult.builtFileInfos,
|
||||
});
|
||||
|
||||
if (!firstSyncResult.success) {
|
||||
return firstSyncResult;
|
||||
}
|
||||
|
||||
onProgress?.('Generating API client...');
|
||||
|
||||
const clientService = new ClientService();
|
||||
|
||||
await clientService.generateCoreClient({ appPath });
|
||||
|
||||
onProgress?.('Running typecheck...');
|
||||
|
||||
const typecheckErrors = await runTypecheck(appPath);
|
||||
|
|
@ -91,31 +73,18 @@ const innerAppBuild = async (
|
|||
};
|
||||
}
|
||||
|
||||
onProgress?.('Rebuilding with generated client...');
|
||||
|
||||
const finalBuildResult = await buildApplication({
|
||||
appPath,
|
||||
const updatedManifest = manifestUpdateChecksums({
|
||||
manifest,
|
||||
filePaths,
|
||||
builtFileInfos: buildResult.builtFileInfos,
|
||||
});
|
||||
|
||||
onProgress?.('Syncing built files...');
|
||||
|
||||
const finalSyncResult = await synchronizeBuiltApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
builtFileInfos: finalBuildResult.builtFileInfos,
|
||||
});
|
||||
|
||||
if (!finalSyncResult.success) {
|
||||
return finalSyncResult;
|
||||
}
|
||||
await writeManifestToOutput(appPath, updatedManifest);
|
||||
|
||||
const outputDir = path.join(appPath, '.twenty', 'output');
|
||||
|
||||
const result: AppBuildResult = {
|
||||
outputDir,
|
||||
fileCount: finalBuildResult.builtFileInfos.size,
|
||||
fileCount: buildResult.builtFileInfos.size,
|
||||
};
|
||||
|
||||
if (options.tarball) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import fs from 'fs';
|
||||
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { runSafe } from '@/cli/utilities/run-safe';
|
||||
import { appBuild } from './build';
|
||||
import { APP_ERROR_CODES, type CommandResult } from '@/cli/types';
|
||||
|
||||
export type AppDeployOptions = {
|
||||
appPath: string;
|
||||
serverUrl: string;
|
||||
tarballPath: string;
|
||||
remote?: string;
|
||||
serverUrl?: string;
|
||||
token?: string;
|
||||
onProgress?: (message: string) => void;
|
||||
};
|
||||
|
|
@ -19,25 +20,19 @@ export type AppDeployResult = {
|
|||
const innerAppDeploy = async (
|
||||
options: AppDeployOptions,
|
||||
): Promise<CommandResult<AppDeployResult>> => {
|
||||
const { appPath, serverUrl, token, onProgress } = options;
|
||||
const { tarballPath, onProgress } = options;
|
||||
|
||||
const buildResult = await appBuild({
|
||||
appPath,
|
||||
tarball: true,
|
||||
onProgress,
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
return buildResult;
|
||||
if (options.remote) {
|
||||
ConfigService.setActiveRemote(options.remote);
|
||||
}
|
||||
|
||||
onProgress?.(`Uploading ${buildResult.data.tarballPath}...`);
|
||||
onProgress?.(`Uploading ${tarballPath}...`);
|
||||
|
||||
const tarballBuffer = fs.readFileSync(buildResult.data.tarballPath!);
|
||||
const tarballBuffer = fs.readFileSync(tarballPath);
|
||||
|
||||
const apiService = new ApiService({
|
||||
serverUrl,
|
||||
token,
|
||||
serverUrl: options.serverUrl,
|
||||
token: options.token,
|
||||
});
|
||||
|
||||
const uploadResult = await apiService.uploadAppTarball({ tarballBuffer });
|
||||
|
|
@ -52,22 +47,6 @@ const innerAppDeploy = async (
|
|||
};
|
||||
}
|
||||
|
||||
onProgress?.('Installing application...');
|
||||
|
||||
const installResult = await apiService.installTarballApp({
|
||||
universalIdentifier: uploadResult.data.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!installResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.DEPLOY_FAILED,
|
||||
message: `Install failed: ${installResult.error}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export { appBuild } from './build';
|
|||
export type { AppBuildOptions, AppBuildResult } from './build';
|
||||
export { appDeploy } from './deploy';
|
||||
export type { AppDeployOptions, AppDeployResult } from './deploy';
|
||||
export { appInstall } from './install';
|
||||
export type { AppInstallOptions } from './install';
|
||||
export { appPublish } from './publish';
|
||||
export type { AppPublishOptions, AppPublishResult } from './publish';
|
||||
export { appUninstall } from './uninstall';
|
||||
|
|
|
|||
57
packages/twenty-sdk/src/cli/operations/install.ts
Normal file
57
packages/twenty-sdk/src/cli/operations/install.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { runSafe } from '@/cli/utilities/run-safe';
|
||||
import { APP_ERROR_CODES, type CommandResult } from '@/cli/types';
|
||||
|
||||
export type AppInstallOptions = {
|
||||
appPath: string;
|
||||
remote?: string;
|
||||
};
|
||||
|
||||
const innerAppInstall = async (
|
||||
options: AppInstallOptions,
|
||||
): Promise<CommandResult> => {
|
||||
if (options.remote) {
|
||||
ConfigService.setActiveRemote(options.remote);
|
||||
}
|
||||
|
||||
const apiService = new ApiService();
|
||||
const manifest = await readManifestFromFile(options.appPath);
|
||||
|
||||
if (!manifest) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.MANIFEST_NOT_FOUND,
|
||||
message: 'Manifest not found. Run `build` or `dev` first.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const result = await apiService.installTarballApp({
|
||||
universalIdentifier: manifest.application.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
const errorMessage =
|
||||
result.error instanceof Error
|
||||
? result.error.message
|
||||
: String(result.error ?? 'Unknown error');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.INSTALL_FAILED,
|
||||
message: errorMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: undefined };
|
||||
};
|
||||
|
||||
export const appInstall = (
|
||||
options: AppInstallOptions,
|
||||
): Promise<CommandResult> =>
|
||||
runSafe(() => innerAppInstall(options), APP_ERROR_CODES.INSTALL_FAILED);
|
||||
|
|
@ -20,6 +20,7 @@ export const APP_ERROR_CODES = {
|
|||
MANIFEST_BUILD_FAILED: 'MANIFEST_BUILD_FAILED',
|
||||
BUILD_FAILED: 'BUILD_FAILED',
|
||||
PUBLISH_FAILED: 'PUBLISH_FAILED',
|
||||
INSTALL_FAILED: 'INSTALL_FAILED',
|
||||
UNINSTALL_FAILED: 'UNINSTALL_FAILED',
|
||||
SYNC_FAILED: 'SYNC_FAILED',
|
||||
TYPECHECK_FAILED: 'TYPECHECK_FAILED',
|
||||
|
|
|
|||
|
|
@ -9,10 +9,7 @@ import {
|
|||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { esbuildOneShotBuild } from '@/cli/utilities/build/common/esbuild-one-shot-build';
|
||||
import {
|
||||
LOGIC_FUNCTION_EXTERNAL_MODULES,
|
||||
createSdkClientsResolverPlugin,
|
||||
} from '@/cli/utilities/build/common/esbuild-watcher';
|
||||
import { LOGIC_FUNCTION_EXTERNAL_MODULES } from '@/cli/utilities/build/common/esbuild-watcher';
|
||||
import { getBaseFrontComponentBuildOptions } from '@/cli/utilities/build/common/front-component-build/utils/get-base-front-component-build-options';
|
||||
import { getFrontComponentBuildPlugins } from '@/cli/utilities/build/common/front-component-build/utils/get-front-component-build-plugins';
|
||||
import { type OnFileBuiltCallback } from '@/cli/utilities/build/common/restartable-watcher-interface';
|
||||
|
|
@ -36,6 +33,7 @@ export type BuiltFileInfo = {
|
|||
builtPath: string;
|
||||
sourcePath: string;
|
||||
fileFolder: FileFolder;
|
||||
usesSdkClient?: boolean;
|
||||
};
|
||||
|
||||
export type AppBuildResult = {
|
||||
|
|
@ -58,6 +56,7 @@ export const buildApplication = async (
|
|||
builtPath: event.builtPath,
|
||||
sourcePath: event.sourcePath,
|
||||
fileFolder: event.fileFolder,
|
||||
usesSdkClient: event.usesSdkClient,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -80,7 +79,6 @@ export const buildApplication = async (
|
|||
metafile: true,
|
||||
logLevel: 'silent',
|
||||
banner: NODE_ESM_CJS_BANNER,
|
||||
plugins: [createSdkClientsResolverPlugin(options.appPath)],
|
||||
},
|
||||
onFileBuilt: collectFileBuilt,
|
||||
});
|
||||
|
|
@ -93,10 +91,11 @@ export const buildApplication = async (
|
|||
...getBaseFrontComponentBuildOptions(),
|
||||
outdir: join(options.appPath, OUTPUT_DIR),
|
||||
tsconfig: join(options.appPath, 'tsconfig.json'),
|
||||
plugins: [
|
||||
createSdkClientsResolverPlugin(options.appPath),
|
||||
...getFrontComponentBuildPlugins(),
|
||||
],
|
||||
jsx: 'automatic',
|
||||
sourcemap: true,
|
||||
metafile: true,
|
||||
logLevel: 'silent',
|
||||
plugins: [...getFrontComponentBuildPlugins()],
|
||||
},
|
||||
onFileBuilt: collectFileBuilt,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import path from 'path';
|
|||
import { type OnFileBuiltCallback } from '@/cli/utilities/build/common/restartable-watcher-interface';
|
||||
import { type FileFolder } from 'twenty-shared/types';
|
||||
|
||||
const SDK_CLIENT_IMPORT_PREFIX = 'twenty-client-sdk';
|
||||
|
||||
export type ProcessEsbuildResultParams = {
|
||||
result: esbuild.BuildResult;
|
||||
appPath: string;
|
||||
|
|
@ -42,12 +44,21 @@ export const processEsbuildResult = async ({
|
|||
|
||||
lastChecksums.set(relativeBuiltPath, checksum);
|
||||
|
||||
const outputMeta = result.metafile?.outputs?.[outputFile];
|
||||
const usesSdkClient =
|
||||
outputMeta?.imports?.some(
|
||||
(imp) =>
|
||||
imp.external === true &&
|
||||
imp.path.startsWith(SDK_CLIENT_IMPORT_PREFIX),
|
||||
) ?? false;
|
||||
|
||||
if (onFileBuilt) {
|
||||
await onFileBuilt({
|
||||
fileFolder,
|
||||
builtPath: relativeBuiltPath,
|
||||
sourcePath: relativeSourcePath,
|
||||
checksum,
|
||||
usesSdkClient,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { CLIENTS_SOURCE_DIR } from '@/cli/constants/clients-dir';
|
||||
import { cleanupRemovedFiles } from '@/cli/utilities/build/common/cleanup-removed-files';
|
||||
import { processEsbuildResult } from '@/cli/utilities/build/common/esbuild-result-processor';
|
||||
import { FRONT_COMPONENT_EXTERNAL_MODULES } from '@/cli/utilities/build/common/front-component-build/constants/front-component-external-modules';
|
||||
|
|
@ -16,6 +15,8 @@ import { NODE_ESM_CJS_BANNER, OUTPUT_DIR } from 'twenty-shared/application';
|
|||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
export const LOGIC_FUNCTION_EXTERNAL_MODULES: string[] = [
|
||||
'twenty-client-sdk/core',
|
||||
'twenty-client-sdk/metadata',
|
||||
'path',
|
||||
'fs',
|
||||
'crypto',
|
||||
|
|
@ -186,25 +187,6 @@ export class EsbuildWatcher implements RestartableWatcher {
|
|||
}
|
||||
}
|
||||
|
||||
// Resolves twenty-sdk/clients to the source barrel so esbuild
|
||||
// bundles it instead of treating it as external (via twenty-sdk/*)
|
||||
export const createSdkClientsResolverPlugin = (
|
||||
appPath: string,
|
||||
): esbuild.Plugin => ({
|
||||
name: 'sdk-clients-resolver',
|
||||
setup: (build) => {
|
||||
build.onResolve({ filter: /^twenty-sdk\/clients/ }, () => ({
|
||||
path: path.join(
|
||||
appPath,
|
||||
'node_modules',
|
||||
'twenty-sdk',
|
||||
CLIENTS_SOURCE_DIR,
|
||||
'index.ts',
|
||||
),
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export type EsbuildWatcherFactoryOptions = RestartableWatcherOptions & {
|
||||
shouldSkipTypecheck: () => boolean;
|
||||
};
|
||||
|
|
@ -220,7 +202,6 @@ export const createLogicFunctionsWatcher = (
|
|||
platform: 'node',
|
||||
extraPlugins: [
|
||||
createTypecheckPlugin(options.appPath, options.shouldSkipTypecheck),
|
||||
createSdkClientsResolverPlugin(options.appPath),
|
||||
],
|
||||
banner: NODE_ESM_CJS_BANNER,
|
||||
},
|
||||
|
|
@ -237,7 +218,6 @@ export const createFrontComponentsWatcher = (
|
|||
jsx: 'automatic',
|
||||
extraPlugins: [
|
||||
createTypecheckPlugin(options.appPath, options.shouldSkipTypecheck),
|
||||
createSdkClientsResolverPlugin(options.appPath),
|
||||
...getFrontComponentBuildPlugins(),
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1 +1,4 @@
|
|||
export const FRONT_COMPONENT_EXTERNAL_MODULES: string[] = [];
|
||||
export const FRONT_COMPONENT_EXTERNAL_MODULES: string[] = [
|
||||
'twenty-client-sdk/core',
|
||||
'twenty-client-sdk/metadata',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export type OnFileBuiltCallback = (options: {
|
|||
builtPath: string;
|
||||
sourcePath: string;
|
||||
checksum: string;
|
||||
usesSdkClient?: boolean;
|
||||
}) => void | Promise<void>;
|
||||
|
||||
export type OnBuildErrorCallback = (
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ const validApplication: ApplicationManifest = {
|
|||
defaultRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2',
|
||||
packageJsonChecksum: '98592af7-4be9-4655-b5c4-9bef307a996c',
|
||||
yarnLockChecksum: '580ee05f-15fe-4146-bac2-6c382483c94e',
|
||||
apiClientChecksum: null,
|
||||
};
|
||||
|
||||
const validField: FieldManifest = {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue